├── .github ├── dependabot.yml └── workflows │ ├── autorelease.yml │ ├── python-test.yml │ └── trigger-build.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.rst ├── VERSION ├── ldeep ├── __init__.py ├── __main__.py ├── utils │ ├── __init__.py │ ├── sddl.py │ └── structure.py └── views │ ├── __init__.py │ ├── activedirectory.py │ ├── cache_activedirectory.py │ ├── constants.py │ ├── ldap_activedirectory.py │ └── structures.py ├── pdm.lock └── pyproject.toml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | 10 | # Maintain dependencies for go 11 | - package-ecosystem: "pip" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/autorelease.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - 'VERSION' 8 | 9 | permissions: 10 | id-token: write 11 | contents: write 12 | 13 | jobs: 14 | linux-build: 15 | name: Linux Build 16 | runs-on: "ubuntu-latest" 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version: "3.12" 22 | 23 | - name: PDM 24 | run: python3.12 -m pip install pdm 25 | 26 | - name: Install libkrb5-dev 27 | run: sudo apt-get install -y libkrb5-dev 28 | 29 | - name: Installs dev deps and package 30 | run : PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 31 | 32 | - name: Build binary release 33 | run: | 34 | pdm run python3.12 -m nuitka --standalone --onefile --assume-yes-for-downloads --output-filename=ldeep.bin ldeep/__main__.py 35 | mv ldeep.bin ldeep_linux-amd64 36 | 37 | - name: Build Source Distribution 38 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm build -d sdist --no-wheel 39 | 40 | - name: Upload Artifacts (binary) 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: linux 44 | path: ldeep_linux-amd64 45 | 46 | - name: Upload Artifacts (sdist) 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: sdist 50 | path: sdist/* 51 | 52 | windows-build: 53 | name: Windows Build 54 | runs-on: "windows-latest" 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/setup-python@v5 58 | with: 59 | python-version: "3.12" 60 | - name: PDM 61 | run: python3 -m pip install pdm 62 | 63 | - name: Installs dev deps and package 64 | run: $env:PDM_BUILD_SCM_VERSION=gc "VERSION"; pdm install --dev 65 | 66 | - name: Build 67 | run: | 68 | pdm run python3 -m nuitka --standalone --assume-yes-for-downloads --output-filename=ldeep.exe --onefile --assume-yes-for-downloads ldeep/__main__.py 69 | mv ldeep.exe ldeep_windows-amd64.exe 70 | - name: Upload Artifacts 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: windows 74 | path: ldeep_windows-amd64.exe 75 | 76 | macos-build: 77 | name: MacOS ARM64 Build 78 | runs-on: "macos-latest" 79 | steps: 80 | - uses: actions/checkout@v4 81 | - uses: actions/setup-python@v5 82 | with: 83 | python-version: "3.12" 84 | 85 | - name: PDM 86 | run: python3.12 -m pip install pdm 87 | 88 | - name: Installs dev deps and package 89 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 90 | 91 | - name: Build 92 | run: | 93 | pdm run python3.12 -m nuitka --standalone --onefile --assume-yes-for-downloads --output-filename=ldeep.bin ldeep/__main__.py 94 | mv ldeep.bin ldeep_macos-arm64 95 | 96 | - name: Upload Artifacts 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: macos 100 | path: ldeep_macos-arm64 101 | 102 | macos-amd-build: 103 | name: MacOS AMD64 Build 104 | runs-on: "macos-13" 105 | steps: 106 | - uses: actions/checkout@v4 107 | - uses: actions/setup-python@v5 108 | with: 109 | python-version: "3.12" 110 | 111 | - name: PDM 112 | run: python3.12 -m pip install pdm 113 | 114 | - name: Installs dev deps and package 115 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 116 | 117 | - name: Build 118 | run: | 119 | pdm run python3.12 -m nuitka --standalone --onefile --assume-yes-for-downloads --output-filename=ldeep.bin ldeep/__main__.py 120 | mv ldeep.bin ldeep_macos-amd64 121 | 122 | - name: Upload Artifacts 123 | uses: actions/upload-artifact@v4 124 | with: 125 | name: macos-amd 126 | path: ldeep_macos-amd64 127 | 128 | tagged-release: 129 | needs: [linux-build, windows-build, macos-build, macos-amd-build] 130 | runs-on: ubuntu-latest 131 | steps: 132 | - name: Checkout 133 | uses: actions/checkout@v4 134 | 135 | - name: Get local version 136 | run: echo "version=$(cat VERSION)" >> $GITHUB_ENV 137 | 138 | - name: Create tag 139 | uses: rickstaa/action-create-tag@v1 140 | with: 141 | tag: ${{ env.version }} 142 | 143 | - name: Download all workflow run artifacts 144 | uses: actions/download-artifact@v4 145 | 146 | - name: Create the release 147 | uses: softprops/action-gh-release@v2 148 | with: 149 | tag_name: ${{ env.version }} 150 | generate_release_notes: true 151 | body: "${{ github.event.head_commit.message }}" 152 | files: | 153 | linux/ldeep_linux-amd64 154 | windows/ldeep_windows-amd64.exe 155 | macos/ldeep_macos-arm64 156 | macos-amd/ldeep_macos-amd64 157 | 158 | - name: Upload to PyPI 159 | uses: pypa/gh-action-pypi-publish@release/v1 160 | with: 161 | packages-dir: sdist/ 162 | -------------------------------------------------------------------------------- /.github/workflows/python-test.yml: -------------------------------------------------------------------------------- 1 | name: Python Unit Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | unit-test: 10 | name: Unit Tests 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Set up Python 3.12 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.12" 21 | 22 | - name: Python Black 23 | uses: psf/black@stable 24 | with: 25 | src: ./ldeep 26 | options: --check --diff 27 | 28 | - name: Set up PDM and Twine 29 | run: python3.12 -m pip install pdm twine 30 | 31 | - name: Check sdist with Twine 32 | run: | 33 | pdm build -d sdist --no-wheel 34 | twine check --strict sdist/* 35 | 36 | - name: Install dependencies 37 | run: | 38 | sudo apt-get install -y libkrb5-dev 39 | pdm install --prod 40 | 41 | - name: Try to run ldeep through PDM 42 | run: | 43 | set +e 44 | echo "\`\`\`console" >> $GITHUB_STEP_SUMMARY 45 | echo pdm run ldeep -h >> $GITHUB_STEP_SUMMARY 46 | pdm run ldeep -h >> $GITHUB_STEP_SUMMARY 2>&1 47 | exitcode="$?" 48 | echo "\`\`\`" >> $GITHUB_STEP_SUMMARY 49 | exit "$exitcode" 50 | -------------------------------------------------------------------------------- /.github/workflows/trigger-build.yml: -------------------------------------------------------------------------------- 1 | name: Manual Test Build 2 | 3 | on: 4 | workflow_dispatch: 5 | branches: [master] 6 | 7 | jobs: 8 | linux-build: 9 | name: Linux Build 10 | runs-on: "ubuntu-latest" 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-python@v5 14 | with: 15 | python-version: "3.12" 16 | 17 | - name: PDM 18 | run: python3.12 -m pip install pdm 19 | 20 | - name: Install libkrb5-dev 21 | run: sudo apt-get install -y libkrb5-dev 22 | 23 | - name: Installs dev deps and package 24 | run : PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 25 | 26 | - name: Build binary release 27 | run: | 28 | pdm run python3.12 -m nuitka --standalone --onefile --output-filename=ldeep.bin ldeep/__main__.py 29 | mv ldeep.bin ldeep_linux-amd64 30 | 31 | - name: Build Source Distribution 32 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm build -d sdist --no-wheel 33 | 34 | - name: Upload Artifacts (binary) 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: linux 38 | path: ldeep_linux-amd64 39 | 40 | - name: Upload Artifacts (sdist) 41 | uses: actions/upload-artifact@v4 42 | with: 43 | name: sdist 44 | path: sdist/* 45 | 46 | windows-build: 47 | name: Windows Build 48 | runs-on: "windows-latest" 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/setup-python@v5 52 | with: 53 | python-version: "3.12" 54 | - name: PDM 55 | run: python3 -m pip install pdm 56 | 57 | - name: Installs dev deps and package 58 | run: $env:PDM_BUILD_SCM_VERSION=gc "VERSION"; pdm install --dev 59 | 60 | - name: Build 61 | run: | 62 | pdm run python3 -m nuitka --standalone --assume-yes-for-downloads --output-filename=ldeep.exe --onefile ldeep/__main__.py 63 | mv ldeep.exe ldeep_windows-amd64.exe 64 | - name: Upload Artifacts 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: windows 68 | path: ldeep_windows-amd64.exe 69 | 70 | macos-build: 71 | name: MacOS ARM64 Build 72 | runs-on: "macos-latest" 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-python@v5 76 | with: 77 | python-version: "3.12" 78 | 79 | - name: PDM 80 | run: python3.12 -m pip install pdm 81 | 82 | - name: Installs dev deps and package 83 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 84 | 85 | - name: Build 86 | run: | 87 | pdm run python3.12 -m nuitka --standalone --onefile --assume-yes-for-downloads --output-filename=ldeep.bin ldeep/__main__.py 88 | mv ldeep.bin ldeep_macos-arm64 89 | 90 | - name: Upload Artifacts 91 | if: always() 92 | uses: actions/upload-artifact@v4 93 | with: 94 | name: macos 95 | path: | 96 | ldeep_macos-arm64 97 | nuitka-crash-report.xml 98 | 99 | macos-amd-build: 100 | name: MacOS AMD64 Build 101 | runs-on: "macos-13" 102 | steps: 103 | - uses: actions/checkout@v4 104 | - uses: actions/setup-python@v5 105 | with: 106 | python-version: "3.12" 107 | 108 | - name: PDM 109 | run: python3.12 -m pip install pdm 110 | 111 | - name: Installs dev deps and package 112 | run: PDM_BUILD_SCM_VERSION=$(cat VERSION) pdm install --dev 113 | 114 | - name: Build 115 | run: | 116 | pdm run python3.12 -m nuitka --standalone --onefile --assume-yes-for-downloads --output-filename=ldeep.bin ldeep/__main__.py 117 | mv ldeep.bin ldeep_macos-amd64 118 | 119 | - name: Upload Artifacts 120 | if: always() 121 | uses: actions/upload-artifact@v4 122 | with: 123 | name: macos-amd 124 | path: | 125 | ldeep_macos-amd64 126 | nuitka-crash-report.xml 127 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | # Compiled python modules. 3 | *.pyc 4 | 5 | # Setuptools distribution folder. 6 | /dist/ 7 | /sdist/ 8 | /build/ 9 | 10 | # Python egg metadata, regenerated from source files by setuptools. 11 | /*.egg-info 12 | 13 | # PDM 14 | .venv 15 | .pdm-python 16 | .pdm-build/ 17 | ldeep/_version.py 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: "v5.0.0" 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-merge-conflict 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: "v0.11.7" 14 | hooks: 15 | - id: ruff 16 | args: 17 | - --exit-non-zero-on-fix 18 | - --select=I 19 | 20 | - repo: https://github.com/psf/black 21 | rev: "25.1.0" 22 | hooks: 23 | - id: black 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build command: docker build -t ldeep . 2 | # Execute with: docker run --rm ldeep $args 3 | 4 | FROM python:3.12-slim 5 | WORKDIR /ldeep 6 | RUN apt-get update && apt-get install -y libkrb5-dev gcc python3-dev 7 | COPY . . 8 | RUN PDM_BUILD_SCM_VERSION=$(cat VERSION) pip install . 9 | ENTRYPOINT [ "ldeep" ] 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 French pentesters club 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = $(wildcard ./ldeep/*.py) 2 | 3 | all: build 4 | 5 | build: 6 | pdm build -d sdist 7 | 8 | pypi: $(SRC) 9 | twine upload sdist/* 10 | 11 | pypi-test: $(SRC) 12 | twine upload --repository testpypi sdist/* 13 | 14 | clean: 15 | @rm -rf build/ sdist/ 16 | 17 | mrproper: clean 18 | @find . -name *.pyc -exec rm '{}' \; 19 | @rm -rf *.egg-info 20 | 21 | export: 22 | pdm lock 23 | pdm export -f requirements --without-hashes --prod > requirements.txt 24 | pdm export -f requirements --without-hashes --dev > requirements-dev.txt 25 | 26 | .PHONY: clean mrproper build 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Project Status 3 | ============== 4 | 5 | .. image:: https://github.com/franc-pentest/ldeep/actions/workflows/autorelease.yml/badge.svg 6 | :target: https://github.com/franc-pentest/ldeep/actions/workflows/autorelease.yml 7 | :alt: Build status 8 | .. image:: https://badgen.net/pypi/v/ldeep 9 | :target: https://pypi.org/project/ldeep/ 10 | :alt: PyPi version 11 | .. image:: https://img.shields.io/pypi/dm/ldeep.svg 12 | :alt: Download rate 13 | :target: https://pypi.org/project/ldeep/ 14 | 15 | 16 | 17 | ============ 18 | Installation 19 | ============ 20 | 21 | To use Kerberos, `ldeep` needs to build native extensions and some headers could be required: 22 | 23 | Debian:: 24 | 25 | sudo apt-get install -y libkrb5-dev krb5-config gcc python3-dev 26 | 27 | ArchLinux:: 28 | 29 | sudo pacman -S krb5 30 | 31 | 32 | ------------------------------------------- 33 | Install from pypi (latest released version) 34 | ------------------------------------------- 35 | 36 | :: 37 | 38 | python -m pip install ldeep 39 | 40 | 41 | ---------------------------------------------------- 42 | Install from GitHub (current state of master branch) 43 | ---------------------------------------------------- 44 | 45 | :: 46 | 47 | python -m pip install git+https://github.com/franc-pentest/ldeep 48 | 49 | =========== 50 | Development 51 | =========== 52 | 53 | Clone the project and install the backend build system `pdm`:: 54 | 55 | python -m pip install pdm 56 | git clone https://github.com/franc-pentest/ldeep && cd ldeep 57 | 58 | --------------------------- 59 | Install an isolated version 60 | --------------------------- 61 | 62 | Clone and install dependencies:: 63 | 64 | pdm install 65 | 66 | Run locally:: 67 | 68 | pdm run ldeep 69 | 70 | ---------------------------------- 71 | Install the package in your system 72 | ---------------------------------- 73 | 74 | :: 75 | 76 | python -m pip install . 77 | 78 | ------------------------------------ 79 | Build source and wheel distributions 80 | ------------------------------------ 81 | 82 | :: 83 | 84 | python -m build 85 | 86 | ===== 87 | ldeep 88 | ===== 89 | 90 | Help is self-explanatory. Let's check it out:: 91 | 92 | $ ldeep -h 93 | usage: ldeep [-h] [--version] [-o OUTFILE] [--security_desc] {ldap,cache} ... 94 | 95 | options: 96 | -h, --help show this help message and exit 97 | --version show program's version number and exit 98 | -o OUTFILE, --outfile OUTFILE 99 | Store the results in a file 100 | --security_desc Enable the retrieval of security descriptors in ldeep results 101 | 102 | Mode: 103 | Available modes 104 | 105 | {ldap,cache} Backend engine to retrieve data 106 | 107 | 108 | `ldeep` can either run against an Active Directory LDAP server or locally on saved files:: 109 | 110 | $ ldeep ldap -u Administrator -p 'password' -d winlab -s ldap://10.0.0.1 all backup/winlab 111 | [+] Retrieving auth_policies output 112 | [+] Retrieving auth_policies verbose output 113 | [+] Retrieving computers output 114 | [+] Retrieving conf output 115 | [+] Retrieving delegations output 116 | [+] Retrieving delegations verbose output 117 | [+] Retrieving delegations verbose output 118 | [+] Retrieving delegations verbose output 119 | [+] Retrieving delegations verbose output 120 | [+] Retrieving domain_policy output 121 | [+] Retrieving gmsa output 122 | [+] Retrieving gpo output 123 | [+] Retrieving groups output 124 | [+] Retrieving groups verbose output 125 | [+] Retrieving machines output 126 | [+] Retrieving machines verbose output 127 | [+] Retrieving ou output 128 | [+] Retrieving pkis output 129 | [+] Retrieving pkis verbose output 130 | [+] Retrieving pso output 131 | [+] Retrieving silos output 132 | [+] Retrieving silos verbose output 133 | [+] Retrieving subnets output 134 | [+] Retrieving subnets verbose output 135 | [+] Retrieving trusts output 136 | [+] Retrieving users output 137 | [+] Retrieving users verbose output 138 | [+] Retrieving users verbose output 139 | [+] Retrieving users verbose output 140 | [+] Retrieving users verbose output 141 | [+] Retrieving users verbose output 142 | [+] Retrieving users verbose output 143 | [+] Retrieving users verbose output 144 | [+] Retrieving users verbose output 145 | [+] Retrieving users verbose output 146 | [+] Retrieving zones output 147 | [+] Retrieving zones verbose output 148 | 149 | $ ldeep cache -d backup -p winlab users 150 | Administrator 151 | [...] 152 | 153 | These two modes have different options: 154 | 155 | ---- 156 | LDAP 157 | ---- 158 | 159 | :: 160 | 161 | $ ldeep ldap -h 162 | usage: ldeep - 1.0.80 ldap [-h] -d DOMAIN -s LDAPSERVER [-b BASE] [-t {ntlm,simple}] [--throttle THROTTLE] [--page_size PAGE_SIZE] [-n] [-u USERNAME] [-p PASSWORD] [-H NTLM] [-k] [--pfx-file PFX_FILE] 163 | [--pfx-pass PFX_PASS] [--cert-pem CERT_PEM] [--key-pem KEY_PEM] [-a] 164 | {auth_policies,bitlockerkeys,computers,conf,delegations,domain_policy,fsmo,gmsa,gpo,groups,machines,ou,pkis,pso,sccm,shadow_principals,silos,smsa,subnets,templates,trusts,users,zones,from_guid,from_sid,laps,memberships,membersof,object,sddl,silo,zone,all,enum_users,search,whoami,add_to_group,change_uac,create_computer,create_user,modify_password,remove_from_group,unlock} 165 | ... 166 | 167 | LDAP mode 168 | 169 | options: 170 | -h, --help show this help message and exit 171 | -d DOMAIN, --domain DOMAIN 172 | The domain as NetBIOS or FQDN 173 | -s LDAPSERVER, --ldapserver LDAPSERVER 174 | The LDAP path (ex : ldap://corp.contoso.com:389) 175 | -b BASE, --base BASE LDAP base for query (by default, this value is pulled from remote Ldap) 176 | -t {ntlm,simple}, --type {ntlm,simple} 177 | Authentication type: ntlm (default) or simple. Simple bind will always be in cleartext with ldap (not ldaps) 178 | --throttle THROTTLE Add a throttle between queries to sneak under detection thresholds (in seconds between queries: argument to the sleep function) 179 | --page_size PAGE_SIZE 180 | Configure the page size used by the engine to query the LDAP server (default: 1000) 181 | -n, --no-encryption Encrypt the communication or not (default: encrypted, except with simple bind and ldap) 182 | 183 | NTLM authentication: 184 | -u USERNAME, --username USERNAME 185 | The username 186 | -p PASSWORD, --password PASSWORD 187 | The password used for the authentication 188 | -H NTLM, --ntlm NTLM NTLM hashes, format is LMHASH:NTHASH 189 | 190 | Kerberos authentication: 191 | -k, --kerberos For Kerberos authentication, ticket file should be pointed by $KRB5NAME env variable 192 | 193 | Certificate authentication: 194 | --pfx-file PFX_FILE PFX file 195 | --pfx-pass PFX_PASS PFX password 196 | --cert-pem CERT_PEM User certificate 197 | --key-pem KEY_PEM User private key 198 | 199 | Anonymous authentication: 200 | -a, --anonymous Perform anonymous binds 201 | 202 | commands: 203 | available commands 204 | 205 | {auth_policies,bitlockerkeys,computers,conf,delegations,domain_policy,fsmo,gmsa,gpo,groups,machines,ou,pkis,pso,sccm,shadow_principals,silos,smsa,subnets,templates,trusts,users,zones,from_guid,from_sid,laps,memberships,membersof,object,sddl,silo,zone,all,enum_users,search,whoami,add_to_group,change_uac,create_computer,create_user,modify_password,remove_from_group,unlock} 206 | auth_policies List the authentication policies configured in the Active Directory. 207 | bitlockerkeys Extract the bitlocker recovery keys. 208 | computers List the computer hostnames and resolve them if --resolve is specify. 209 | conf Dump the configuration partition of the Active Directory. 210 | delegations List accounts configured for any kind of delegation. 211 | domain_policy Return the domain policy. 212 | fsmo List FSMO roles. 213 | gmsa List the gmsa accounts and retrieve secrets(NT + kerberos keys) if possible. 214 | gpo Return the list of Group policy objects. 215 | groups List the groups. 216 | machines List the machine accounts. 217 | ou Return the list of organizational units with linked GPO. 218 | pkis List pkis. 219 | pso List the Password Settings Objects. 220 | sccm List servers related to SCCM infrastructure (Primary/Secondary Sites and Distribution Points). 221 | shadow_principals List the shadow principals and the groups associated with. 222 | silos List the silos configured in the Active Directory. 223 | smsa List the smsa accounts and the machines they are associated with. 224 | subnets List sites and associated subnets. 225 | templates List certificate templates. 226 | trusts List the domain's trust relationships. 227 | users List users according to a filter. 228 | zones List the DNS zones configured in the Active Directory. 229 | from_guid Return the object associated with the given `guid`. 230 | from_sid Return the object associated with the given `sid`. 231 | laps Return the LAPS passwords. If a target is specified, only retrieve the LAPS password for this one. 232 | memberships List the group for which `account` belongs to. 233 | membersof List the members of `group`. 234 | object Return the records containing `object` in a CN. 235 | sddl Returns the SDDL of an object given it's CN. 236 | silo Get information about a specific `silo`. 237 | zone Return the records of a DNS zone. 238 | all Collect and store computers, domain_policy, zones, gpo, groups, ou, users, trusts, pso information 239 | enum_users Anonymously enumerate users with LDAP pings. 240 | search Query the LDAP with `filter` and retrieve ALL or `attributes` if specified. 241 | whoami Return user identity. 242 | add_to_group Add `user` to `group`. 243 | change_uac Change user account control 244 | create_computer Create a computer account 245 | create_user Create a user account 246 | modify_password Change `user`'s password. 247 | remove_from_group Remove `user` from `group`. 248 | unlock Unlock `user`. 249 | 250 | 251 | 252 | ----- 253 | CACHE 254 | ----- 255 | 256 | :: 257 | 258 | $ ldeep cache -h 259 | usage: ldeep cache [-h] [-d DIR] -p PREFIX 260 | {auth_policies,bitlockerkeys,computers,conf,delegations,domain_policy,fsmo,gmsa,gpo,groups,machines,ou,pkis,pso,sccm,shadow_principals,silos,smsa,subnets,trusts,users,zones,from_guid,from_sid,laps,memberships,membersof,object,sddl,silo,zone} 261 | ... 262 | 263 | Cache mode 264 | 265 | options: 266 | -h, --help show this help message and exit 267 | -d DIR, --dir DIR Use saved JSON files in specified directory as cache 268 | -p PREFIX, --prefix PREFIX 269 | Prefix of ldeep saved files 270 | 271 | commands: 272 | available commands 273 | 274 | {auth_policies,bitlockerkeys,computers,conf,delegations,domain_policy,fsmo,gmsa,gpo,groups,machines,ou,pkis,pso,sccm,shadow_principals,silos,smsa,subnets,trusts,users,zones,from_guid,from_sid,laps,memberships,membersof,object,sddl,silo,zone} 275 | auth_policies List the authentication policies configured in the Active Directory. 276 | bitlockerkeys Extract the bitlocker recovery keys. 277 | computers List the computer hostnames and resolve them if --resolve is specify. 278 | conf Dump the configuration partition of the Active Directory. 279 | delegations List accounts configured for any kind of delegation. 280 | domain_policy Return the domain policy. 281 | fsmo List FSMO roles. 282 | gmsa List the gmsa accounts and retrieve NT hash if possible. 283 | gpo Return the list of Group policy objects. 284 | groups List the groups. 285 | machines List the machine accounts. 286 | ou Return the list of organizational units with linked GPO. 287 | pkis List pkis. 288 | pso List the Password Settings Objects. 289 | sccm List servers related to SCCM infrastructure (Primary/Secondary Sites and Distribution Points). 290 | shadow_principals List the shadow principals and the groups associated with. 291 | silos List the silos configured in the Active Directory. 292 | smsa List the smsa accounts and the machines they are associated with. 293 | subnets List sites and associated subnets. 294 | trusts List the domain's trust relationships. 295 | users List users according to a filter. 296 | zones List the DNS zones configured in the Active Directory. 297 | from_guid Return the object associated with the given `guid`. 298 | from_sid Return the object associated with the given `sid`. 299 | laps Return the LAPS passwords. If a target is specified, only retrieve the LAPS password for this one. 300 | memberships List the group for which `account` belongs to. 301 | membersof List the members of `group`. 302 | object Return the records containing `object` in a CN. 303 | sddl Returns the SDDL of an object given it's CN. 304 | silo Get information about a specific `silo`. 305 | zone Return the records of a DNS zone. 306 | 307 | 308 | 309 | 310 | ============== 311 | Usage examples 312 | ============== 313 | 314 | Listing users without verbosity:: 315 | 316 | $ ldeep ldap -u Administrator -p 'password' -d winlab.local -s ldap://10.0.0.1 users 317 | userspn2 318 | userspn1 319 | gobobo 320 | test 321 | krbtgt 322 | DefaultAccount 323 | Guest 324 | Administrator 325 | 326 | 327 | Listing users with reversible password encryption enable and with verbosity:: 328 | 329 | $ ldeep ldap -u Administrator -p 'password' -d winlab.local -s ldap://10.0.0.1 users reversible -v 330 | [ 331 | { 332 | "accountExpires": "9999-12-31T23:59:59.999999", 333 | "badPasswordTime": "1601-01-01T00:00:00+00:00", 334 | "badPwdCount": 0, 335 | "cn": "User SPN1", 336 | "codePage": 0, 337 | "countryCode": 0, 338 | "dSCorePropagationData": [ 339 | "1601-01-01T00:00:00+00:00" 340 | ], 341 | "displayName": "User SPN1", 342 | "distinguishedName": "CN=User SPN1,CN=Users,DC=winlab,DC=local", 343 | "dn": "CN=User SPN1,CN=Users,DC=winlab,DC=local", 344 | "givenName": "User", 345 | "instanceType": 4, 346 | "lastLogoff": "1601-01-01T00:00:00+00:00", 347 | "lastLogon": "1601-01-01T00:00:00+00:00", 348 | "logonCount": 0, 349 | "msDS-SupportedEncryptionTypes": 0, 350 | "name": "User SPN1", 351 | "objectCategory": "CN=Person,CN=Schema,CN=Configuration,DC=winlab,DC=local", 352 | "objectClass": [ 353 | "top", 354 | "person", 355 | "organizationalPerson", 356 | "user" 357 | ], 358 | "objectGUID": "{593cb08f-3cc5-431a-b3d7-9fbad4511b1e}", 359 | "objectSid": "S-1-5-21-3640577749-2924176383-3866485758-1112", 360 | "primaryGroupID": 513, 361 | "pwdLastSet": "2018-10-13T12:19:30.099674+00:00", 362 | "sAMAccountName": "userspn1", 363 | "sAMAccountType": "SAM_GROUP_OBJECT | SAM_NON_SECURITY_GROUP_OBJECT | SAM_ALIAS_OBJECT | SAM_NON_SECURITY_ALIAS_OBJECT | SAM_USER_OBJECT | SAM_NORMAL_USER_ACCOUNT | SAM_MACHINE_ACCOUNT | SAM_TRUST_ACCOUNT | SAM_ACCOUNT_TYPE_MAX", 364 | "servicePrincipalName": [ 365 | "HOST/blah" 366 | ], 367 | "sn": "SPN1", 368 | "uSNChanged": 115207, 369 | "uSNCreated": 24598, 370 | "userAccountControl": "ENCRYPTED_TEXT_PWD_ALLOWED | NORMAL_ACCOUNT | DONT_REQ_PREAUTH", 371 | "userPrincipalName": "userspn1@winlab.local", 372 | "whenChanged": "2018-10-22T18:04:43+00:00", 373 | "whenCreated": "2018-10-13T12:19:30+00:00" 374 | } 375 | ] 376 | 377 | Listing GPOs:: 378 | 379 | $ ldeep -u Administrator -p 'password' -d winlab.local -s ldap://10.0.0.1 gpo 380 | {6AC1786C-016F-11D2-945F-00C04fB984F9}: Default Domain Controllers Policy 381 | {31B2F340-016D-11D2-945F-00C04FB984F9}: Default Domain Policy 382 | 383 | Getting all things:: 384 | 385 | $ ldeep ldap -u Administrator -p 'password' -d winlab.local -s ldap://10.0.0.1 all /tmp/winlab.local_dump 386 | [+] Retrieving computers output 387 | [+] Retrieving domain_policy output 388 | [+] Retrieving gpo output 389 | [+] Retrieving groups output 390 | [+] Retrieving groups verbose output 391 | [+] Retrieving ou output 392 | [+] Retrieving pso output 393 | [+] Retrieving trusts output 394 | [+] Retrieving users output 395 | [+] Retrieving users verbose output 396 | [+] Retrieving zones output 397 | [+] Retrieving zones verbose output 398 | 399 | Using this last command line switch, you have persistent output in both verbose and non-verbose mode saved:: 400 | 401 | $ ls winlab.local_dump_* 402 | winlab.local_dump_computers.lst winlab.local_dump_groups.json winlab.local_dump_pso.lst winlab.local_dump_users.lst 403 | winlab.local_dump_domain_policy.lst winlab.local_dump_groups.lst winlab.local_dump_trusts.lst winlab.local_dump_zones.json 404 | winlab.local_dump_gpo.lst winlab.local_dump_ou.lst winlab.local_dump_users.json winlab.local_dump_zones.lst 405 | 406 | The the cache mode can be used to query some other information. 407 | 408 | 409 | -------------------------- 410 | Usage with Kerberos config 411 | -------------------------- 412 | 413 | For Kerberos, you will also need to configure the ``/etc/krb5.conf``.:: 414 | 415 | [realms] 416 | CORP.LOCAL = { 417 | kdc = DC01.CORP.LOCAL 418 | } 419 | 420 | ======== 421 | Upcoming 422 | ======== 423 | 424 | * Proper DNS zone enumeration 425 | * ADCS enumeration 426 | * Project tree 427 | * Any ideas? 428 | 429 | ================ 430 | Related projects 431 | ================ 432 | 433 | * https://github.com/SecureAuthCorp/impacket 434 | * https://github.com/ropnop/windapsearch 435 | * https://github.com/shellster/LDAPPER 436 | 437 | 438 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.86 2 | -------------------------------------------------------------------------------- /ldeep/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franc-pentest/ldeep/0968a2c45967969c09f04243191bd545f68b096b/ldeep/__init__.py -------------------------------------------------------------------------------- /ldeep/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.dummy import Pool as ThreadPool 2 | from sys import __stderr__, __stdout__, exit 3 | 4 | import dns.resolver 5 | from termcolor import colored 6 | from tqdm import tqdm 7 | 8 | 9 | def info(content): 10 | __stderr__.write("%s\n" % colored("[+] " + str(content), "blue", attrs=["bold"])) 11 | 12 | 13 | def error(content, close_array=False): 14 | __stderr__.write("%s\n" % colored("[!] " + str(content), "red", attrs=["bold"])) 15 | if close_array: 16 | print("]") 17 | 18 | 19 | class Logger(object): 20 | def __init__(self, outfile=None, quiet=False): 21 | self.quiet = quiet 22 | self.terminal = __stdout__ 23 | self.log = open(outfile, "w") if outfile else None 24 | 25 | def write(self, message): 26 | if not self.quiet: 27 | self.terminal.write(message) 28 | if self.log: 29 | self.log.write(message) 30 | 31 | def flush(self): 32 | if self.log: 33 | self.log.flush() 34 | pass 35 | 36 | 37 | class ResolverThread(object): 38 | def __init__(self, dns_server): 39 | self.dns_server = dns_server 40 | self.resolutions = [] 41 | 42 | def resolve(self, hostname): 43 | if self.dns_server: 44 | resolver = dns.resolver.Resolver() 45 | resolver.nameservers = [self.dns_server] 46 | else: 47 | resolver = dns.resolver 48 | try: 49 | answers = resolver.query(hostname, "A", tcp=True) 50 | for rdata in answers: 51 | if rdata.address: 52 | self.resolutions.append( 53 | {"hostname": hostname, "address": rdata.address} 54 | ) 55 | break 56 | else: 57 | pass 58 | except Exception: 59 | pass 60 | 61 | 62 | def resolve(hostnames, dns_server): 63 | pool = ThreadPool(20) 64 | resolver_thread = ResolverThread(dns_server) 65 | with tqdm(total=len(hostnames)) as pbar: 66 | for _ in pool.imap_unordered( 67 | resolver_thread.resolve, 68 | tqdm( 69 | hostnames, 70 | desc="Resolution", 71 | bar_format="{desc} {n_fmt}/{total_fmt} hostnames", 72 | ), 73 | ): 74 | pbar.update() 75 | pool.close() 76 | pool.join() 77 | results_set = [] 78 | for computer in resolver_thread.resolutions: 79 | results_set.append(computer) 80 | return results_set 81 | -------------------------------------------------------------------------------- /ldeep/utils/sddl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | A module used to handle binary ntSecurityDescriptor from Active Directory LDAP. 5 | """ 6 | 7 | from struct import unpack 8 | 9 | from ldap3.protocol.formatters.formatters import format_sid, format_uuid_le 10 | 11 | SDDLTypeFlags = { 12 | "Self Relative": 0b1000000000000000, 13 | "RM Control Valid": 0b0100000000000000, 14 | "SACL Protected": 0b0010000000000000, 15 | "DACL Protected": 0b0001000000000000, 16 | "SACL Auto Inherit": 0b0000100000000000, 17 | "DACL Auto Inherit": 0b0000010000000000, 18 | "SACL Auto Inherit Required": 0b0000001000000000, 19 | "DACL Auto Inherit Required": 0b0000000100000000, 20 | "Server Security": 0b0000000010000000, 21 | "DACL Trusted": 0b0000000001000000, 22 | "SACL Defaulted": 0b0000000000100000, 23 | "SACL Present": 0b0000000000010000, 24 | "DACL Defaulted": 0b0000000000001000, 25 | "DACL Present": 0b0000000000000100, 26 | "Group Defaulted": 0b0000000000000010, 27 | "Owner Defaulted": 0b0000000000000001, 28 | } 29 | 30 | SID_SIZE = 28 31 | 32 | 33 | def parse_ntSecurityDescriptor(input_buffer): 34 | """Parses a ntSecurityDescriptor.""" 35 | out = dict() 36 | fields = ( 37 | "Revision", 38 | "Raw Type", 39 | "Offset to owner SID", 40 | "Offset to group SID", 41 | "Offset to SACL", 42 | "Offset to DACL", 43 | ) 44 | 45 | for k, v in zip(fields, unpack(" [big endian] 49 | 50 | usual printf like specifiers can be used (if started with %) 51 | [not recommended, there is no way to unpack this] 52 | 53 | %08x will output an 8 bytes hex 54 | %s will output a string 55 | %s\\x00 will output a NUL terminated string 56 | %d%d will output 2 decimal digits (against the very same specification of Structure) 57 | ... 58 | 59 | some additional format specifiers: 60 | : just copy the bytes from the field into the output string (input may be string, other structure, or anything responding to __str__()) (for unpacking, all what's left is returned) 61 | z same as :, but adds a NUL byte at the end (asciiz) (for unpacking the first NUL byte is used as terminator) [asciiz string] 62 | u same as z, but adds two NUL bytes at the end (after padding to an even size with NULs). (same for unpacking) [unicode string] 63 | w DCE-RPC/NDR string (it's a macro for [ ' 2: 156 | dataClassOrCode = field[2] 157 | try: 158 | self[field[0]] = self.unpack( 159 | field[1], 160 | data[:size], 161 | dataClassOrCode=dataClassOrCode, 162 | field=field[0], 163 | ) 164 | except Exception as e: 165 | e.args += ( 166 | "When unpacking field '%s | %s | %r[:%d]'" 167 | % (field[0], field[1], data, size), 168 | ) 169 | raise 170 | 171 | size = self.calcPackSize(field[1], self[field[0]], field[0]) 172 | if self.alignment and size % self.alignment: 173 | size += self.alignment - (size % self.alignment) 174 | data = data[size:] 175 | 176 | return self 177 | 178 | def __setitem__(self, key, value): 179 | self.fields[key] = value 180 | self.data = None # force recompute 181 | 182 | def __getitem__(self, key): 183 | return self.fields[key] 184 | 185 | def __delitem__(self, key): 186 | del self.fields[key] 187 | 188 | def __str__(self): 189 | return self.getData() 190 | 191 | def __len__(self): 192 | # XXX: improve 193 | return len(self.getData()) 194 | 195 | def pack(self, format, data, field=None): 196 | if self.debug: 197 | print(" pack( %s | %r | %s)" % (format, data, field)) 198 | 199 | if field: 200 | addressField = self.findAddressFieldFor(field) 201 | if (addressField is not None) and (data is None): 202 | return b"" 203 | 204 | # void specifier 205 | if format[:1] == "_": 206 | return b"" 207 | 208 | # quote specifier 209 | if format[:1] == "'" or format[:1] == '"': 210 | return b(format[1:]) 211 | 212 | # code specifier 213 | two = format.split("=") 214 | if len(two) >= 2: 215 | try: 216 | return self.pack(two[0], data) 217 | except: 218 | fields = {"self": self} 219 | fields.update(self.fields) 220 | return self.pack(two[0], eval(two[1], {}, fields)) 221 | 222 | # address specifier 223 | two = format.split("&") 224 | if len(two) == 2: 225 | try: 226 | return self.pack(two[0], data) 227 | except: 228 | if (two[1] in self.fields) and (self[two[1]] is not None): 229 | return self.pack( 230 | two[0], id(self[two[1]]) & ((1 << (calcsize(two[0]) * 8)) - 1) 231 | ) 232 | else: 233 | return self.pack(two[0], 0) 234 | 235 | # length specifier 236 | two = format.split("-") 237 | if len(two) == 2: 238 | try: 239 | return self.pack(two[0], data) 240 | except: 241 | return self.pack(two[0], self.calcPackFieldSize(two[1])) 242 | 243 | # array specifier 244 | two = format.split("*") 245 | if len(two) == 2: 246 | answer = bytes() 247 | for each in data: 248 | answer += self.pack(two[1], each) 249 | if two[0]: 250 | if two[0].isdigit(): 251 | if int(two[0]) != len(data): 252 | raise Exception( 253 | "Array field has a constant size, and it doesn't match the actual value" 254 | ) 255 | else: 256 | return self.pack(two[0], len(data)) + answer 257 | return answer 258 | 259 | # "printf" string specifier 260 | if format[:1] == "%": 261 | # format string like specifier 262 | return b(format % data) 263 | 264 | # asciiz specifier 265 | if format[:1] == "z": 266 | if isinstance(data, bytes): 267 | return data + b("\0") 268 | return bytes(b(data) + b("\0")) 269 | 270 | # unicode specifier 271 | if format[:1] == "u": 272 | return bytes(data + b("\0\0") + (len(data) & 1 and b("\0") or b"")) 273 | 274 | # DCE-RPC/NDR string specifier 275 | if format[:1] == "w": 276 | if len(data) == 0: 277 | data = b("\0\0") 278 | elif len(data) % 2: 279 | data = b(data) + b("\0") 280 | l = pack("= 2: 347 | return self.unpack(two[0], data) 348 | 349 | # length specifier 350 | two = format.split("-") 351 | if len(two) == 2: 352 | return self.unpack(two[0], data) 353 | 354 | # array specifier 355 | two = format.split("*") 356 | if len(two) == 2: 357 | answer = [] 358 | sofar = 0 359 | if two[0].isdigit(): 360 | number = int(two[0]) 361 | elif two[0]: 362 | sofar += self.calcUnpackSize(two[0], data) 363 | number = self.unpack(two[0], data[:sofar]) 364 | else: 365 | number = -1 366 | 367 | while number and sofar < len(data): 368 | nsofar = sofar + self.calcUnpackSize(two[1], data[sofar:]) 369 | answer.append(self.unpack(two[1], data[sofar:nsofar], dataClassOrCode)) 370 | number -= 1 371 | sofar = nsofar 372 | return answer 373 | 374 | # "printf" string specifier 375 | if format[:1] == "%": 376 | # format string like specifier 377 | return format % data 378 | 379 | # asciiz specifier 380 | if format == "z": 381 | if data[-1:] != b("\x00"): 382 | raise Exception( 383 | "%s 'z' field is not NUL terminated: %r" % (field, data) 384 | ) 385 | if PY3: 386 | return data[:-1].decode("latin-1") 387 | else: 388 | return data[:-1] 389 | 390 | # unicode specifier 391 | if format == "u": 392 | if data[-2:] != b("\x00\x00"): 393 | raise Exception( 394 | "%s 'u' field is not NUL-NUL terminated: %r" % (field, data) 395 | ) 396 | return data[:-2] # remove trailing NUL 397 | 398 | # DCE-RPC/NDR string specifier 399 | if format == "w": 400 | l = unpack("= 2: 436 | return self.calcPackSize(two[0], data) 437 | 438 | # length specifier 439 | two = format.split("-") 440 | if len(two) == 2: 441 | return self.calcPackSize(two[0], data) 442 | 443 | # array specifier 444 | two = format.split("*") 445 | if len(two) == 2: 446 | answer = 0 447 | if two[0].isdigit(): 448 | if int(two[0]) != len(data): 449 | raise Exception( 450 | "Array field has a constant size, and it doesn't match the actual value" 451 | ) 452 | elif two[0]: 453 | answer += self.calcPackSize(two[0], len(data)) 454 | 455 | for each in data: 456 | answer += self.calcPackSize(two[1], each) 457 | return answer 458 | 459 | # "printf" string specifier 460 | if format[:1] == "%": 461 | # format string like specifier 462 | return len(format % data) 463 | 464 | # asciiz specifier 465 | if format[:1] == "z": 466 | return len(data) + 1 467 | 468 | # asciiz specifier 469 | if format[:1] == "u": 470 | l = len(data) 471 | return l + (l & 1 and 3 or 2) 472 | 473 | # DCE-RPC/NDR string specifier 474 | if format[:1] == "w": 475 | l = len(data) 476 | return 12 + l + l % 2 477 | 478 | # literal specifier 479 | if format[:1] == ":": 480 | return len(data) 481 | 482 | # struct like specifier 483 | return calcsize(format) 484 | 485 | def calcUnpackSize(self, format, data, field=None): 486 | if self.debug: 487 | print(" calcUnpackSize( %s | %s | %r)" % (field, format, data)) 488 | 489 | # void specifier 490 | if format[:1] == "_": 491 | return 0 492 | 493 | addressField = self.findAddressFieldFor(field) 494 | if addressField is not None: 495 | if not self[addressField]: 496 | return 0 497 | 498 | try: 499 | lengthField = self.findLengthFieldFor(field) 500 | return int(self[lengthField]) 501 | except Exception: 502 | pass 503 | 504 | # XXX: Try to match to actual values, raise if no match 505 | 506 | # quote specifier 507 | if format[:1] == "'" or format[:1] == '"': 508 | return len(format) - 1 509 | 510 | # address specifier 511 | two = format.split("&") 512 | if len(two) == 2: 513 | return self.calcUnpackSize(two[0], data) 514 | 515 | # code specifier 516 | two = format.split("=") 517 | if len(two) >= 2: 518 | return self.calcUnpackSize(two[0], data) 519 | 520 | # length specifier 521 | two = format.split("-") 522 | if len(two) == 2: 523 | return self.calcUnpackSize(two[0], data) 524 | 525 | # array specifier 526 | two = format.split("*") 527 | if len(two) == 2: 528 | answer = 0 529 | if two[0]: 530 | if two[0].isdigit(): 531 | number = int(two[0]) 532 | else: 533 | answer += self.calcUnpackSize(two[0], data) 534 | number = self.unpack(two[0], data[:answer]) 535 | 536 | while number: 537 | number -= 1 538 | answer += self.calcUnpackSize(two[1], data[answer:]) 539 | else: 540 | while answer < len(data): 541 | answer += self.calcUnpackSize(two[1], data[answer:]) 542 | return answer 543 | 544 | # "printf" string specifier 545 | if format[:1] == "%": 546 | raise Exception( 547 | "Can't guess the size of a printf like specifier for unpacking" 548 | ) 549 | 550 | # asciiz specifier 551 | if format[:1] == "z": 552 | return data.index(b("\x00")) + 1 553 | 554 | # asciiz specifier 555 | if format[:1] == "u": 556 | l = data.index(b("\x00\x00")) 557 | return l + (l & 1 and 3 or 2) 558 | 559 | # DCE-RPC/NDR string specifier 560 | if format[:1] == "w": 561 | l = unpack("?@[\\]^_`{|}~ " 650 | ): 651 | return chr(x) 652 | else: 653 | return "." 654 | 655 | 656 | def hexdump(data, indent=""): 657 | if data is None: 658 | return 659 | if isinstance(data, int): 660 | data = str(data).encode("utf-8") 661 | x = bytearray(data) 662 | strLen = len(x) 663 | i = 0 664 | while i < strLen: 665 | line = " %s%04x " % (indent, i) 666 | for j in range(16): 667 | if i + j < strLen: 668 | line += "%02X " % x[i + j] 669 | else: 670 | line += " " 671 | if j % 16 == 7: 672 | line += " " 673 | line += " " 674 | line += "".join(pretty_print(x) for x in x[i : i + 16]) 675 | print(line) 676 | i += 16 677 | 678 | 679 | def parse_bitmask(dict, value): 680 | ret = "" 681 | 682 | for i in range(0, 31): 683 | flag = 1 << i 684 | 685 | if value & flag == 0: 686 | continue 687 | 688 | if flag in dict: 689 | ret += "%s | " % dict[flag] 690 | else: 691 | ret += "0x%.8X | " % flag 692 | 693 | if len(ret) == 0: 694 | return "0" 695 | else: 696 | return ret[:-3] 697 | -------------------------------------------------------------------------------- /ldeep/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franc-pentest/ldeep/0968a2c45967969c09f04243191bd545f68b096b/ldeep/views/__init__.py -------------------------------------------------------------------------------- /ldeep/views/activedirectory.py: -------------------------------------------------------------------------------- 1 | from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES 2 | from ldap3.protocol.formatters.validators import validate_guid, validate_sid 3 | 4 | ALL_ATTRIBUTES = ALL_ATTRIBUTES 5 | ALL_OPERATIONAL_ATTRIBUTES = ALL_OPERATIONAL_ATTRIBUTES 6 | ALL = [ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES] 7 | validate_sid = validate_sid 8 | validate_guid = validate_guid 9 | 10 | 11 | class ActiveDirectoryView(object): 12 | """ 13 | Manage a view of a Active Directory. 14 | """ 15 | 16 | class ActiveDirectoryInvalidSID(Exception): 17 | pass 18 | 19 | class ActiveDirectoryInvalidGUID(Exception): 20 | pass 21 | -------------------------------------------------------------------------------- /ldeep/views/cache_activedirectory.py: -------------------------------------------------------------------------------- 1 | from json import load as json_load 2 | from os import path 3 | 4 | from ldeep.views.activedirectory import ( 5 | ALL, 6 | ALL_ATTRIBUTES, 7 | ActiveDirectoryView, 8 | validate_guid, 9 | validate_sid, 10 | ) 11 | from ldeep.views.constants import WELL_KNOWN_SIDS 12 | 13 | FILE_CONTENT_DICT = dict() 14 | 15 | 16 | class UnexpectedFormatException(Exception): 17 | pass 18 | 19 | 20 | # case insenitive equal when x and y are str instances 21 | def eq(x, y): 22 | if isinstance(x, str) and isinstance(y, str): 23 | return x.lower() == y.lower() 24 | else: 25 | return x == y 26 | 27 | 28 | # respect the ANR field 29 | def eq_anr(record, value): 30 | def fmap(f, obj): 31 | if isinstance(obj, dict): 32 | return any(fmap(f, sub) for sub in obj.values()) 33 | elif isinstance(obj, list): 34 | return any(fmap(f, k) for k in obj) 35 | elif isinstance(obj, str): 36 | return f(obj) 37 | else: 38 | raise UnexpectedFormatException( 39 | f"Unexpected value, expected: dict, list or str, obtained: {type(obj)}." 40 | ) 41 | 42 | validate = lambda x: x.lower().startswith(value.lower()) 43 | 44 | keys = [ 45 | "displayName", 46 | "givenName", 47 | "legacyExchangeDN", 48 | "physicalDeliveryOfficeName", 49 | "proxyAddresses", 50 | "Name", 51 | "sAMAccountName", 52 | "sn", 53 | ] 54 | for k in keys: 55 | if k in record and fmap(validate, record[k]): 56 | return True 57 | 58 | 59 | class CacheActiveDirectoryView(ActiveDirectoryView): 60 | # Constant functions (first arg -> self but we don't need it) 61 | USER_LOCKED_FILTER = lambda _: {"files": ["users_locked"]} 62 | GROUPS_FILTER = lambda _: {"files": ["groups"]} 63 | USER_ALL_FILTER = lambda _: {"files": ["users_all"]} 64 | USER_SPN_FILTER = lambda _: {"files": ["users_spn"]} 65 | COMPUTERS_FILTER = lambda _: {"files": ["machines"]} 66 | ANR = lambda _, u: { 67 | "files": ["users_all", "groups", "machines"], 68 | "filter": lambda record: eq_anr(record, u), 69 | } 70 | GROUP_DN_FILTER = lambda _, g: { 71 | "fmt": "json", 72 | "files": ["groups"], 73 | "filter": lambda x: eq(x["sAMAccountName"], g), 74 | } 75 | ACCOUNTS_IN_GROUP_FILTER = lambda _, p, g: { 76 | "fmt": "json", 77 | "files": ["users_all", "groups", "machines"], 78 | "filter": lambda x: ("primaryGroupID" in x and eq(p, x["primaryGroupID"])) 79 | or ("memberOf" in x and g in x["memberOf"]), 80 | } 81 | ACCOUNT_IN_GROUPS_FILTER = lambda _, u: { 82 | "fmt": "json", 83 | "files": ["users_all", "groups", "machines"], 84 | "filter": lambda x: eq(x["sAMAccountName"], u), 85 | } 86 | DISTINGUISHED_NAME = lambda _, n: { 87 | "fmt": "json", 88 | "files": ["users_all", "groups", "machines"], 89 | "filter": lambda x: eq(x["distinguishedName"], n), 90 | } 91 | PRIMARY_GROUP_ID = lambda _, i: { 92 | "fmt": "json", 93 | "files": ["users_all", "groups", "machines"], 94 | "filter": lambda x: x["objectSid"].endswith(f"-{i}"), 95 | } 96 | AUTH_POLICIES_FILTER = lambda _: {"files": ["auth_policies"]} 97 | SILOS_FILTER = lambda _: {"files": ["silos"]} 98 | SILO_FILTER = lambda _, s: { 99 | "fmt": "json", 100 | "files": ["silos"], 101 | "filter": lambda x: eq(x["cn"], s), 102 | } 103 | ZONES_FILTER = lambda _: { 104 | "fmt": "json", 105 | "files": ["zones"], 106 | } 107 | ZONE_FILTER = lambda _: { 108 | "fmt": "json", 109 | "files": ["dns_records"], 110 | } 111 | 112 | # Not implemented: 113 | DOMAIN_INFO_FILTER = lambda _: None 114 | GPO_INFO_FILTER = lambda _: None 115 | OU_FILTER = lambda _: None 116 | PSO_INFO_FILTER = lambda _: None 117 | TRUSTS_INFO_FILTER = lambda _: None 118 | USER_ACCOUNT_CONTROL_FILTER = lambda _, __: None 119 | USER_ACCOUNT_CONTROL_FILTER_NEG = lambda _, __: None 120 | USER_LOCKED_FILTER = lambda _: None 121 | SMSA_FILTER = lambda _: None 122 | SHADOW_PRINCIPALS_FILTER = lambda _: None 123 | UNCONSTRAINED_DELEGATION_FILTER = lambda _: None 124 | CONSTRAINED_DELEGATION_FILTER = lambda _: None 125 | RESOURCE_BASED_CONSTRAINED_DELEGATION_FILTER = lambda _: None 126 | ALL_DELEGATIONS_FILTER = lambda _: None 127 | 128 | class CacheActiveDirectoryException(Exception): 129 | pass 130 | 131 | class CacheActiveDirectoryDirNotFoundException(Exception): 132 | pass 133 | 134 | def __init__(self, cache_dir=".", prefix="ldeep_"): 135 | """ 136 | CacheActiveDirectoryView constructor. 137 | Initialize the cache state with the provided directory and file prefixes. 138 | 139 | @cache_dir: directory containing ldeep files 140 | @prefix: prefix of the files 141 | """ 142 | if not path.exists(cache_dir): 143 | raise self.CacheActiveDirectoryDirNotFoundException( 144 | f"{cache_dir} doesn't exist." 145 | ) 146 | self.path = cache_dir 147 | self.prefix = prefix 148 | self.fqdn, self.base_dn, self.forest_base_dn = self.__get_domain_info() 149 | self.attributes = ALL 150 | self.throttle = 0 151 | self.page_size = 0 152 | 153 | def set_all_attributes(self, attributes=ALL): 154 | self.attributes = attributes 155 | 156 | def all_attributes(self): 157 | return self.attributes 158 | 159 | def set_controls(self, controls): 160 | pass 161 | 162 | def query(self, cachefilter, attributes=[], base=None, scope=None, **filter_args): 163 | """ 164 | Perform a query to cache files. 165 | 166 | @cachefilter: a dict containing the following fields: fmt (optional), files and filter (optional). 167 | @attributes: only use to deduce the file formats to use (`lst` or `json`). 168 | @base: Not implemented. 169 | @scope: Not implemented. 170 | 171 | @return a list of records. 172 | """ 173 | 174 | def scrub_json_from_key(obj, func): 175 | if isinstance(obj, dict): 176 | for key in list(obj.keys()): 177 | if func(key): 178 | del obj[key] 179 | else: 180 | scrub_json_from_key(obj[key], func) 181 | elif isinstance(obj, list): 182 | for k in reversed(range(len(obj))): 183 | if func(obj[k]): 184 | del obj[k] 185 | else: 186 | scrub_json_from_key(obj[k], func) 187 | 188 | # Process unimplemented queries 189 | if cachefilter is None: 190 | raise self.CacheActiveDirectoryException("Cache query not supported.") 191 | 192 | if base is not None: 193 | if "filter" in cachefilter: 194 | oldFilter = cachefilter["filter"] 195 | cachefilter["filter"] = lambda elt: oldFilter(elt) and ( 196 | "dn" not in elt or elt["dn"].endswith("," + base) 197 | ) 198 | else: 199 | cachefilter["filter"] = lambda elt: "dn" not in elt or elt[ 200 | "dn" 201 | ].endswith("," + base) 202 | 203 | # Get format of cache files to use: either `lst` or `json` 204 | if "fmt" in cachefilter: 205 | fmt = cachefilter["fmt"] 206 | elif ALL_ATTRIBUTES in attributes: 207 | fmt = "json" 208 | else: 209 | fmt = "lst" 210 | 211 | data = [] 212 | # For each file, retrieve result based on an optional filter 213 | for fil in cachefilter["files"]: 214 | filename = "{prefix}_{file}.{ext}".format( 215 | prefix=self.prefix, file=fil, ext=fmt 216 | ) 217 | 218 | # Two cases 219 | # all attributes are required thus we parse the JSON file 220 | if fmt == "json": 221 | if path.join(self.path, filename) in FILE_CONTENT_DICT: 222 | json = FILE_CONTENT_DICT[path.join(self.path, filename)] 223 | else: 224 | fp = open(path.join(self.path, filename)) 225 | json = json_load(fp) 226 | if "ntSecurityDescriptor" not in self.attributes: 227 | scrub_json_from_key(json, lambda x: x == "nTSecurityDescriptor") 228 | FILE_CONTENT_DICT[path.join(self.path, filename)] = json 229 | 230 | if "filter" in cachefilter: 231 | for record in json: 232 | if cachefilter["filter"](record): 233 | data.append(record) 234 | else: 235 | data += json 236 | 237 | # we use the lst file 238 | else: 239 | if path.join(self.path, filename) in FILE_CONTENT_DICT: 240 | fp = FILE_CONTENT_DICT[path.join(self.path, filename)] 241 | else: 242 | fp = open(path.join(self.path, filename)) 243 | FILE_CONTENT_DICT[path.join(self.path, filename)] = fp 244 | if "filter" in cachefilter: 245 | for line in fp: 246 | x = {"sAMAccountName": line.strip()} # a little hacky :) 247 | if cachefilter["filter"](x): 248 | data += [line.strip()] 249 | else: 250 | data += map(lambda x: x.strip(), fp.readlines()) 251 | return data 252 | 253 | def query_server_info(self): 254 | return self.query( 255 | { 256 | "fmt": "json", 257 | "files": ["server_info"], 258 | } 259 | ) 260 | 261 | def resolve_sid(self, sid): 262 | """ 263 | Two cases: 264 | * the SID is a WELL KNOWN SID and a local SID, the name of the corresponding account is returned; 265 | * else, the SID is search through the cache and the corresponding record is returned. 266 | 267 | @sid: the sid to search for. 268 | 269 | @throw ActiveDirectoryInvalidSID if the SID is not a valid SID. 270 | @return the record corresponding to the SID queried. 271 | """ 272 | if sid in WELL_KNOWN_SIDS: 273 | return WELL_KNOWN_SIDS[sid] 274 | elif validate_sid(sid): 275 | results = self.query( 276 | { 277 | "fmt": "json", 278 | "files": ["users_all", "groups", "machines"], 279 | "filter": lambda x: x["objectSid"] == sid, 280 | } 281 | ) 282 | if results: 283 | return results 284 | raise self.ActiveDirectoryInvalidSID(f"SID: {sid}") 285 | 286 | def resolve_guid(self, guid): 287 | """ 288 | Return the cache record with the provided GUID. 289 | 290 | @guid: the guid to search for. 291 | 292 | @throw ActiveDirectoryInvalidGUID if the GUID is not a valid GUID. 293 | @return the record corresponding to the guid queried. 294 | """ 295 | if validate_guid(guid): 296 | results = self.query( 297 | { 298 | "fmt": "json", 299 | "files": ["users_all", "groups", "machines"], 300 | "filter": lambda x: x["objectGUID"] == guid, 301 | } 302 | ) 303 | if results: 304 | return results 305 | raise self.ActiveDirectoryInvalidGUID(f"GUID: {guid}") 306 | 307 | def get_sddl(self, *kwargs): 308 | raise NotImplementedError 309 | 310 | def __get_domain_info(self): 311 | """ 312 | Private functions to retrieve the cache domain name. 313 | """ 314 | filename = "{prefix}_server_info.json".format(prefix=self.prefix) 315 | with open(path.join(self.path, filename)) as fp: 316 | info = json_load(fp) 317 | base = info[0]["raw"]["defaultNamingContext"][0] 318 | forest_base = info[0]["raw"]["rootDomainNamingContext"][0] 319 | domain = base.replace("DC=", ".")[1:].replace(",", "") 320 | return domain, base, forest_base 321 | -------------------------------------------------------------------------------- /ldeep/views/constants.py: -------------------------------------------------------------------------------- 1 | DNS_TYPES = { 2 | "ZERO": 0x0000, 3 | "A": 0x0001, 4 | "NS": 0x0002, 5 | "MD": 0x0003, 6 | "MF": 0x0004, 7 | "CNAME": 0x0005, 8 | "SOA": 0x0006, 9 | "MB": 0x0007, 10 | "MG": 0x0008, 11 | "MR": 0x0009, 12 | "NULL": 0x000A, 13 | "WKS": 0x000B, 14 | "PTR": 0x000C, 15 | "HINFO": 0x000D, 16 | "MINFO": 0x000E, 17 | "MX": 0x000F, 18 | "TXT": 0x0010, 19 | "RP": 0x0011, 20 | "AFSDB": 0x0012, 21 | "X25": 0x0013, 22 | "ISDN": 0x0014, 23 | "RT": 0x0015, 24 | "SIG": 0x0018, 25 | "KEY": 0x0019, 26 | "AAAA": 0x001C, 27 | "LOC": 0x001D, 28 | "NXT": 0x001E, 29 | "SRV": 0x0021, 30 | "ATMA": 0x0022, 31 | "NAPTR": 0x0023, 32 | "DNAME": 0x0027, 33 | "DS": 0x002B, 34 | "RRSIG": 0x002E, 35 | "NSEC": 0x002F, 36 | "DNSKEY": 0x0030, 37 | "DHCID": 0x0031, 38 | "NSEC3": 0x0032, 39 | "NSEC3PARAM": 0x0033, 40 | "TLSA": 0x0034, 41 | "ALL": 0x00FF, 42 | "WINS": 0xFF01, 43 | "WINSR": 0xFF02, 44 | } 45 | 46 | USER_ACCOUNT_CONTROL = { 47 | "SCRIPT": 0x0001, 48 | "ACCOUNTDISABLE": 0x0002, 49 | "HOMEDIR_REQUIRED": 0x0008, 50 | "LOCKOUT": 0x0010, 51 | "PASSWD_NOTREQD": 0x0020, 52 | "PASSWD_CANT_CHANGE": 0x0040, 53 | "ENCRYPTED_TEXT_PWD_ALLOWED": 0x0080, 54 | "TEMP_DUPLICATE_ACCOUNT": 0x0100, 55 | "NORMAL_ACCOUNT": 0x0200, 56 | "INTERDOMAIN_TRUST_ACCOUNT": 0x0800, 57 | "WORKSTATION_TRUST_ACCOUNT": 0x1000, 58 | "SERVER_TRUST_ACCOUNT": 0x2000, 59 | "DONT_EXPIRE_PASSWORD": 0x10000, 60 | "MNS_LOGON_ACCOUNT": 0x20000, 61 | "SMARTCARD_REQUIRED": 0x40000, 62 | "TRUSTED_FOR_DELEGATION": 0x80000, 63 | "NOT_DELEGATED": 0x100000, 64 | "USE_DES_KEY_ONLY": 0x200000, 65 | "DONT_REQ_PREAUTH": 0x400000, 66 | "PASSWORD_EXPIRED": 0x800000, 67 | "TRUSTED_TO_AUTH_FOR_DELEGATION": 0x1000000, 68 | "PARTIAL_SECRETS_ACCOUNT": 0x04000000, 69 | } 70 | 71 | SAM_ACCOUNT_TYPE = { 72 | "SAM_DOMAIN_OBJECT": 0x0, 73 | "SAM_GROUP_OBJECT": 0x10000000, 74 | "SAM_NON_SECURITY_GROUP_OBJECT": 0x10000001, 75 | "SAM_ALIAS_OBJECT": 0x20000000, 76 | "SAM_NON_SECURITY_ALIAS_OBJECT": 0x20000001, 77 | "SAM_USER_OBJECT": 0x30000000, 78 | "SAM_NORMAL_USER_ACCOUNT": 0x30000000, 79 | "SAM_MACHINE_ACCOUNT": 0x30000001, 80 | "SAM_TRUST_ACCOUNT": 0x30000002, 81 | "SAM_APP_BASIC_GROUP": 0x40000000, 82 | "SAM_APP_QUERY_GROUP": 0x40000001, 83 | "SAM_ACCOUNT_TYPE_MAX": 0x7FFFFFFF, 84 | } 85 | 86 | PWD_PROPERTIES = { 87 | "DOMAIN_PASSWORD_COMPLEX": 0x1, 88 | "DOMAIN_PASSWORD_NO_ANON_CHANGE": 0x2, 89 | "DOMAIN_PASSWORD_NO_CLEAR_CHANGE": 0x4, 90 | "DOMAIN_LOCKOUT_ADMINS": 0x8, 91 | "DOMAIN_PASSWORD_STORE_CLEARTEXT": 0x10, 92 | "DOMAIN_REFUSE_PASSWORD_CHANGE": 0x20, 93 | } 94 | 95 | TRUSTS_INFOS = { 96 | "NON_TRANSITIVE": 0x1, 97 | "UPLEVEL_ONLY": 0x2, 98 | "QUARANTINED_DOMAIN": 0x4, 99 | "FOREST_TRANSITIVE": 0x8, 100 | "CROSS_ORGANIZATION": 0x10, 101 | "WITHIN_FOREST": 0x20, 102 | "TREAT_AS_EXTERNAL": 0x40, 103 | "USES_RC4_ENCRYPTION": 0x80, 104 | "USES_AES_KEYS": 0x100, 105 | "CROSS_ORGANIZATION_NO_TGT_DELEGATION": 0x200, 106 | "PIM_TRUST": 0x400, 107 | "CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION": 0x800, 108 | } 109 | 110 | FOREST_LEVELS = { 111 | 7: "Windows Server 2016", 112 | 6: "Windows Server 2012 R2", 113 | 5: "Windows Server 2012", 114 | 4: "Windows Server 2008 R2", 115 | 3: "Windows Server 2008", 116 | 2: "Windows Server 2003", 117 | 1: "Windows Server 2003 operating system through Windows Server 2016", 118 | 0: "Windows 2000 Server operating system through Windows Server 2008 operating system", 119 | } 120 | 121 | WELL_KNOWN_SIDS = { 122 | "S-1-5-11": "Authenticated Users", 123 | "S-1-5-32-544": r"BUILTIN\Administrators", 124 | "S-1-5-32-545": r"BUILTIN\Users", 125 | "S-1-5-32-546": r"BUILTIN\Guests", 126 | "S-1-5-32-547": r"BUILTIN\Power Users", 127 | "S-1-5-32-548": r"BUILTIN\Account Operators", 128 | "S-1-5-32-549": r"BUILTIN\Server Operators", 129 | "S-1-5-32-550": r"BUILTIN\Print Operators", 130 | "S-1-5-32-551": r"BUILTIN\Backup Operators", 131 | "S-1-5-32-552": r"BUILTIN\Replicators", 132 | "S-1-5-64-10": r"BUILTIN\NTLM Authentication", 133 | "S-1-5-64-14": r"BUILTIN\SChannel Authentication", 134 | "S-1-5-64-21": r"BUILTIN\Digest Authentication", 135 | "S-1-16-4096": r"BUILTIN\Low Mandatory Level", 136 | "S-1-16-8192": r"BUILTIN\Medium Mandatory Level", 137 | "S-1-16-8448": r"BUILTIN\Medium Plus Mandatory Level", 138 | "S-1-16-12288": r"BUILTIN\High Mandatory Level", 139 | "S-1-16-16384": r"BUILTIN\System Mandatory Level", 140 | "S-1-16-20480": r"BUILTIN\Protected Process Mandatory Level", 141 | "S-1-16-28672": r"BUILTIN\Secure Process Mandatory Level", 142 | "S-1-5-32-554": r"BUILTIN\Pre-Windows 2000 Compatible Access", 143 | "S-1-5-32-555": r"BUILTIN\Remote Desktop Users", 144 | "S-1-5-32-556": r"BUILTIN\Network Configuration Operators", 145 | "S-1-5-32-557": r"BUILTIN\Incoming Forest Trust Builders", 146 | "S-1-5-32-558": r"BUILTIN\Performance Monitor Users", 147 | "S-1-5-32-559": r"BUILTIN\Performance Log Users", 148 | "S-1-5-32-560": r"BUILTIN\Windows Authorization Access Group", 149 | "S-1-5-32-561": r"BUILTIN\Terminal Server License Servers", 150 | "S-1-5-32-562": r"BUILTIN\Distributed COM Users", 151 | "S-1-5-32-569": r"BUILTIN\Cryptographic Operators", 152 | "S-1-5-32-573": r"BUILTIN\Event Log Readers", 153 | "S-1-5-32-574": r"BUILTIN\Certificate Service DCOM Access", 154 | "S-1-5-32-575": r"BUILTIN\RDS Remote Access Servers", 155 | "S-1-5-32-576": r"BUILTIN\RDS Endpoint Servers", 156 | "S-1-5-32-577": r"BUILTIN\RDS Management Servers", 157 | "S-1-5-32-578": r"BUILTIN\Hyper-V Administrators", 158 | "S-1-5-32-579": r"BUILTIN\Access Control Assistance Operators", 159 | "S-1-5-32-580": r"BUILTIN\Remote Management Users", 160 | } 161 | 162 | FILETIME_FIELDS = [ 163 | "badPasswordTime", 164 | "lastLogon", 165 | "lastLogoff", 166 | "lastLogonTimestamp", 167 | "pwdLastSet", 168 | "accountExpires", 169 | "lockoutTime", 170 | ] 171 | 172 | DATETIME_FIELDS = ["dSCorePropagationData", "whenChanged", "whenCreated"] 173 | 174 | FILETIME_TIMESTAMP_FIELDS = { 175 | "lockOutObservationWindow": (60, "mins"), 176 | "lockoutDuration": (60, "mins"), 177 | "maxPwdAge": (86400, "days"), 178 | "minPwdAge": (86400, "days"), 179 | "forceLogoff": (60, "mins"), 180 | } 181 | 182 | LDAP_SERVER_SD_FLAGS_OID_SEC_DESC = [ 183 | ("1.2.840.113556.1.4.801", True, b"\x30\x03\x02\x01\x07") 184 | ] 185 | 186 | LOGON_SAM_LOGON_RESPONSE_EX = b"\x17\x00" 187 | 188 | GMSA_ENCRYPTION_CONSTANTS = b"\x6b\x65\x72\x62\x65\x72\x6f\x73\x7b\x9b\x5b\x2b\x93\x13\x2b\x93\x5c\x9b\xdc\xda\xd9\x5c\x98\x99\xc4\xca\xe4\xde\xe6\xd6\xca\xe4" 189 | 190 | ADRights = { 191 | "GenericRead": 0x00020094, 192 | "GenericWrite": 0x00020028, 193 | "GenericExecute": 0x00020004, 194 | "GenericAll": 0x000F01FF, 195 | "Synchronize": 0x00100000, 196 | "WriteOwner": 0x00080000, 197 | "WriteDacl": 0x00040000, 198 | "ReadControl": 0x00020000, 199 | "Delete": 0x00010000, 200 | "ExtendedRight": 0x00000100, 201 | "CreateChild": 0x00000001, 202 | "DeleteChild": 0x00000002, 203 | "ReadProperty": 0x00000010, 204 | "WriteProperty": 0x00000020, 205 | "Self": 0x00000008, 206 | } 207 | 208 | EXTENDED_RIGHTS_MAP = { 209 | "ab721a52-1e2f-11d0-9819-00aa0040529b": "Domain-Administer-Serve", 210 | "ab721a53-1e2f-11d0-9819-00aa0040529b": "User-Change-Password", 211 | "00299570-246d-11d0-a768-00aa006e0529": "User-Force-Change-Password", 212 | "ab721a54-1e2f-11d0-9819-00aa0040529b": "Send-As", 213 | "ab721a56-1e2f-11d0-9819-00aa0040529b": "Receive-As", 214 | "ab721a55-1e2f-11d0-9819-00aa0040529b": "Send-To", 215 | "c7407360-20bf-11d0-a768-00aa006e0529": "Domain-Password", 216 | "59ba2f42-79a2-11d0-9020-00c04fc2d3cf": "General-Information", 217 | "4c164200-20c0-11d0-a768-00aa006e0529": "User-Account-Restrictions", 218 | "5f202010-79a5-11d0-9020-00c04fc2d4cf": "User-Logon", 219 | "bc0ac240-79a9-11d0-9020-00c04fc2d4cf": "Membership", 220 | "a1990816-4298-11d1-ade2-00c04fd8d5cd": "Open-Address-Book", 221 | "77b5b886-944a-11d1-aebd-0000f80367c1": "Personal-Information", 222 | "e45795b2-9455-11d1-aebd-0000f80367c1": "Email-Information", 223 | "e45795b3-9455-11d1-aebd-0000f80367c1": "Web-Information", 224 | "1131f6aa-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes", 225 | "1131f6ab-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Synchronize", 226 | "1131f6ac-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Manage-Topology", 227 | "e12b56b6-0a95-11d1-adbb-00c04fd8d5cd": "Change-Schema-Maste", 228 | "d58d5f36-0a98-11d1-adbb-00c04fd8d5cd": "Change-Rid-Maste", 229 | "fec364e0-0a98-11d1-adbb-00c04fd8d5cd": "Do-Garbage-Collection", 230 | "0bc1554e-0a99-11d1-adbb-00c04fd8d5cd": "Recalculate-Hierarchy", 231 | "1abd7cf8-0a99-11d1-adbb-00c04fd8d5cd": "Allocate-Rids", 232 | "bae50096-4752-11d1-9052-00c04fc2d4cf": "Change-PDC", 233 | "440820ad-65b4-11d1-a3da-0000f875ae0d": "Add-GUID", 234 | "014bf69c-7b3b-11d1-85f6-08002be74fab": "Change-Domain-Maste", 235 | "e48d0154-bcf8-11d1-8702-00c04fb96050": "Public-Information", 236 | "4b6e08c0-df3c-11d1-9c86-006008764d0e": "msmq-Receive-Dead-Lette", 237 | "4b6e08c1-df3c-11d1-9c86-006008764d0e": "msmq-Peek-Dead-Lette", 238 | "4b6e08c2-df3c-11d1-9c86-006008764d0e": "msmq-Receive-computer-Journal", 239 | "4b6e08c3-df3c-11d1-9c86-006008764d0e": "msmq-Peek-computer-Journal", 240 | "06bd3200-df3e-11d1-9c86-006008764d0e": "msmq-Receive", 241 | "06bd3201-df3e-11d1-9c86-006008764d0e": "msmq-Peek", 242 | "06bd3202-df3e-11d1-9c86-006008764d0e": "msmq-Send", 243 | "06bd3203-df3e-11d1-9c86-006008764d0e": "msmq-Receive-journal", 244 | "b4e60130-df3f-11d1-9c86-006008764d0e": "msmq-Open-Connecto", 245 | "edacfd8f-ffb3-11d1-b41d-00a0c968f939": "Apply-Group-Policy", 246 | "037088f8-0ae1-11d2-b422-00a0c968f939": "RAS-Information", 247 | "9923a32a-3607-11d2-b9be-0000f87a36b2": "DS-Install-Replica", 248 | "cc17b1fb-33d9-11d2-97d4-00c04fd8d5cd": "Change-Infrastructure-Maste", 249 | "be2bb760-7f46-11d2-b9ad-00c04f79f805": "Update-Schema-Cache", 250 | "62dd28a8-7f46-11d2-b9ad-00c04f79f805": "Recalculate-Security-Inheritance", 251 | "69ae6200-7f46-11d2-b9ad-00c04f79f805": "DS-Check-Stale-Phantoms", 252 | "0e10c968-78fb-11d2-90d4-00c04f79dc55": "Enroll", 253 | "bf9679c0-0de6-11d0-a285-00aa003049e2": "Self-Membership", 254 | "72e39547-7b18-11d1-adef-00c04fd8d5cd": "DNS-Host-Name-Attributes", 255 | "f3a64788-5306-11d1-a9c5-0000f80367c1": "Validated-SPN", 256 | "b7b1b3dd-ab09-4242-9e30-9980e5d322f7": "Generate-RSoP-Planning", 257 | "9432c620-033c-4db7-8b58-14ef6d0bf477": "Refresh-Group-Cache", 258 | "91d67418-0135-4acc-8d79-c08e857cfbec": "SAM-Enumerate-Entire-Domain", 259 | "b7b1b3de-ab09-4242-9e30-9980e5d322f7": "Generate-RSoP-Logging", 260 | "b8119fd0-04f6-4762-ab7a-4986c76b3f9a": "Domain-Other-Parameters", 261 | "e2a36dc9-ae17-47c3-b58b-be34c55ba633": "Create-Inbound-Forest-Trust", 262 | "1131f6ad-9c07-11d1-f79f-00c04fc2dcd2": "DS-Replication-Get-Changes-All", 263 | "ba33815a-4f93-4c76-87f3-57574bff8109": "Migrate-SID-History", 264 | "45ec5156-db7e-47bb-b53f-dbeb2d03c40f": "Reanimate-Tombstones", 265 | "68b1d179-0d15-4d4f-ab71-46152e79a7bc": "Allowed-To-Authenticate", 266 | "2f16c4a5-b98e-432c-952a-cb388ba33f2e": "DS-Execute-Intentions-Script", 267 | "f98340fb-7c5b-4cdb-a00b-2ebdfa115a96": "DS-Replication-Monitor-Topology", 268 | "280f369c-67c7-438e-ae98-1d46f3c6f541": "Update-Password-Not-Required-Bit", 269 | "ccc2dc7d-a6ad-4a7a-8846-c04e3cc53501": "Unexpire-Password", 270 | "05c74c5e-4deb-43b4-bd9f-86664c2a7fd5": "Enable-Per-User-Reversibly-Encrypted-Password", 271 | "4ecc03fe-ffc0-4947-b630-eb672a8a9dbc": "DS-Query-Self-Quota", 272 | "91e647de-d96f-4b70-9557-d63ff4f3ccd8": "Private-Information", 273 | "1131f6ae-9c07-11d1-f79f-00c04fc2dcd2": "Read-Only-Replication-Secret-Synchronization", 274 | "ffa6f046-ca4b-4feb-b40d-04dfee722543": "MS-TS-GatewayAccess", 275 | "5805bc62-bdc9-4428-a5e2-856a0f4c185e": "Terminal-Server-License-Serve", 276 | "1a60ea8d-58a6-4b20-bcdc-fb71eb8a9ff8": "Reload-SSL-Certificate", 277 | "89e95b76-444d-4c62-991a-0facbeda640c": "DS-Replication-Get-Changes-In-Filtered-Set", 278 | "7726b9d5-a4b4-4288-a6b2-dce952e80a7f": "Run-Protect-Admin-Groups-Task", 279 | "7c0e2a7c-a419-48e4-a995-10180aad54dd": "Manage-Optional-Features", 280 | "3e0f7e18-2c7a-4c10-ba82-4d926db99a3e": "DS-Clone-Domain-Controlle", 281 | "d31a8757-2447-4545-8081-3bb610cacbf2": "Validated-MS-DS-Behavior-Version", 282 | "80863791-dbe9-4eb8-837e-7f0ab55d9ac7": "Validated-MS-DS-Additional-DNS-Host-Name", 283 | "a05b8cc2-17bc-4802-a710-e7c15ab866a2": "AutoEnroll", 284 | "4125c71f-7fac-4ff0-bcb7-f09a41325286": "DS-Set-Owne", 285 | "88a9933e-e5c8-4f2a-9dd7-2527416b8092": "DS-Bypass-Quota", 286 | "084c93a2-620d-4879-a836-f0ae47de0e89": "DS-Read-Partition-Secrets", 287 | "94825a8d-b171-4116-8146-1e34d8f54401": "DS-Write-Partition-Secrets", 288 | "9b026da6-0d3c-465c-8bee-5199d7165cba": "DS-Validated-Write-Compute", 289 | "00000000-0000-0000-0000-000000000000": "All-Extended-Rights", 290 | } 291 | 292 | EXTENDED_RIGHTS_NAME_MAP = {k: v for v, k in EXTENDED_RIGHTS_MAP.items()} 293 | 294 | OID_TO_STR_MAP = { 295 | "1.3.6.1.4.1.311.76.6.1": "Windows Update", 296 | "1.3.6.1.4.1.311.10.3.11": "Key Recovery", 297 | "1.3.6.1.4.1.311.10.3.25": "Windows Third Party Application Component", 298 | "1.3.6.1.4.1.311.21.6": "Key Recovery Agent", 299 | "1.3.6.1.4.1.311.10.3.6": "Windows System Component Verification", 300 | "1.3.6.1.4.1.311.61.4.1": "Early Launch Antimalware Drive", 301 | "1.3.6.1.4.1.311.10.3.23": "Windows TCB Component", 302 | "1.3.6.1.4.1.311.61.1.1": "Kernel Mode Code Signing", 303 | "1.3.6.1.4.1.311.10.3.26": "Windows Software Extension Verification", 304 | "2.23.133.8.3": "Attestation Identity Key Certificate", 305 | "1.3.6.1.4.1.311.76.3.1": "Windows Store", 306 | "1.3.6.1.4.1.311.10.6.1": "Key Pack Licenses", 307 | "1.3.6.1.4.1.311.20.2.2": "Smart Card Logon", 308 | "1.3.6.1.5.2.3.5": "KDC Authentication", 309 | "1.3.6.1.5.5.7.3.7": "IP security use", 310 | "1.3.6.1.4.1.311.10.3.8": "Embedded Windows System Component Verification", 311 | "1.3.6.1.4.1.311.10.3.20": "Windows Kits Component", 312 | "1.3.6.1.5.5.7.3.6": "IP security tunnel termination", 313 | "1.3.6.1.4.1.311.10.3.5": "Windows Hardware Driver Verification", 314 | "1.3.6.1.5.5.8.2.2": "IP security IKE intermediate", 315 | "1.3.6.1.4.1.311.10.3.39": "Windows Hardware Driver Extended Verification", 316 | "1.3.6.1.4.1.311.10.6.2": "License Server Verification", 317 | "1.3.6.1.4.1.311.10.3.5.1": "Windows Hardware Driver Attested Verification", 318 | "1.3.6.1.4.1.311.76.5.1": "Dynamic Code Generato", 319 | "1.3.6.1.5.5.7.3.8": "Time Stamping", 320 | "1.3.6.1.4.1.311.10.3.4.1": "File Recovery", 321 | "1.3.6.1.4.1.311.2.6.1": "SpcRelaxedPEMarkerCheck", 322 | "2.23.133.8.1": "Endorsement Key Certificate", 323 | "1.3.6.1.4.1.311.2.6.2": "SpcEncryptedDigestRetryCount", 324 | "1.3.6.1.4.1.311.10.3.4": "Encrypting File System", 325 | "1.3.6.1.5.5.7.3.1": "Server Authentication", 326 | "1.3.6.1.4.1.311.61.5.1": "HAL Extension", 327 | "1.3.6.1.5.5.7.3.4": "Secure Email", 328 | "1.3.6.1.5.5.7.3.5": "IP security end system", 329 | "1.3.6.1.4.1.311.10.3.9": "Root List Signe", 330 | "1.3.6.1.4.1.311.10.3.30": "Disallowed List", 331 | "1.3.6.1.4.1.311.10.3.19": "Revoked List Signe", 332 | "1.3.6.1.4.1.311.10.3.21": "Windows RT Verification", 333 | "1.3.6.1.4.1.311.10.3.10": "Qualified Subordination", 334 | "1.3.6.1.4.1.311.10.3.12": "Document Signing", 335 | "1.3.6.1.4.1.311.10.3.24": "Protected Process Verification", 336 | "1.3.6.1.4.1.311.80.1": "Document Encryption", 337 | "1.3.6.1.4.1.311.10.3.22": "Protected Process Light Verification", 338 | "1.3.6.1.4.1.311.21.19": "Directory Service Email Replication", 339 | "1.3.6.1.4.1.311.21.5": "Private Key Archival", 340 | "1.3.6.1.4.1.311.10.5.1": "Digital Rights", 341 | "1.3.6.1.4.1.311.10.3.27": "Preview Build Signing", 342 | "1.3.6.1.4.1.311.20.2.1": "Certificate Request Agent", 343 | "2.23.133.8.2": "Platform Certificate", 344 | "1.3.6.1.4.1.311.20.1": "CTL Usage", 345 | "1.3.6.1.5.5.7.3.9": "OCSP Signing", 346 | "1.3.6.1.5.5.7.3.3": "Code Signing", 347 | "1.3.6.1.4.1.311.10.3.1": "Microsoft Trust List Signing", 348 | "1.3.6.1.4.1.311.10.3.2": "Microsoft Time Stamping", 349 | "1.3.6.1.4.1.311.76.8.1": "Microsoft Publishe", 350 | "1.3.6.1.5.5.7.3.2": "Client Authentication", 351 | "1.3.6.1.5.2.3.4": "PKINIT Client Authentication", 352 | "1.3.6.1.4.1.311.10.3.13": "Lifetime Signing", 353 | "2.5.29.37.0": "Any Purpose", 354 | "1.3.6.1.4.1.311.64.1.1": "Server Trust", 355 | "1.3.6.1.4.1.311.10.3.7": "OEM Windows System Component Verification", 356 | } 357 | 358 | AUTHENTICATING_EKUS = { 359 | "1.3.6.1.5.5.7.3.2": "Client Authentication", 360 | "1.3.6.1.5.2.3.4": "PKINIT Client Authentication", 361 | "1.3.6.1.4.1.311.20.2.2": "Smart Card Logon", 362 | "2.5.29.37.0": "Any Purpose", 363 | } 364 | 365 | MS_PKI_CERTIFICATE_NAME_FLAG = { 366 | "NONE": 0x00000000, 367 | "ENROLLEE_SUPPLIES_SUBJECT": 0x00000001, 368 | "ADD_EMAIL": 0x00000002, 369 | "ADD_OBJ_GUID": 0x00000004, 370 | "OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME": 0x00000008, 371 | "ADD_DIRECTORY_PATH": 0x00000100, 372 | "ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME": 0x00010000, 373 | "SUBJECT_ALT_REQUIRE_DOMAIN_DNS": 0x00400000, 374 | "SUBJECT_ALT_REQUIRE_SPN": 0x00800000, 375 | "SUBJECT_ALT_REQUIRE_DIRECTORY_GUID": 0x01000000, 376 | "SUBJECT_ALT_REQUIRE_UPN": 0x02000000, 377 | "SUBJECT_ALT_REQUIRE_EMAIL": 0x04000000, 378 | "SUBJECT_ALT_REQUIRE_DNS": 0x08000000, 379 | "SUBJECT_REQUIRE_DNS_AS_CN": 0x10000000, 380 | "SUBJECT_REQUIRE_EMAIL": 0x20000000, 381 | "SUBJECT_REQUIRE_COMMON_NAME": 0x40000000, 382 | "SUBJECT_REQUIRE_DIRECTORY_PATH": 0x80000000, 383 | } 384 | 385 | MS_PKI_ENROLLMENT_FLAG = { 386 | "NONE": 0x00000000, 387 | "INCLUDE_SYMMETRIC_ALGORITHMS": 0x00000001, 388 | "PEND_ALL_REQUESTS": 0x00000002, 389 | "PUBLISH_TO_KRA_CONTAINER": 0x00000004, 390 | "PUBLISH_TO_DS": 0x00000008, 391 | "AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE": 0x00000010, 392 | "AUTO_ENROLLMENT": 0x00000020, 393 | "CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED": 0x80, 394 | "PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT": 0x00000040, 395 | "USER_INTERACTION_REQUIRED": 0x00000100, 396 | "ADD_TEMPLATE_NAME": 0x200, 397 | "REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE": 0x00000400, 398 | "ALLOW_ENROLL_ON_BEHALF_OF": 0x00000800, 399 | "ADD_OCSP_NOCHECK": 0x00001000, 400 | "ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL": 0x00002000, 401 | "NOREVOCATIONINFOINISSUEDCERTS": 0x00004000, 402 | "INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS": 0x00008000, 403 | "ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT": 0x00010000, 404 | "ISSUANCE_POLICIES_FROM_REQUEST": 0x00020000, 405 | "SKIP_AUTO_RENEWAL": 0x00040000, 406 | "NO_SECURITY_EXTENSION": 0x0008000, 407 | } 408 | -------------------------------------------------------------------------------- /ldeep/views/ldap_activedirectory.py: -------------------------------------------------------------------------------- 1 | from json import loads as json_loads 2 | from socket import AF_INET6, inet_ntoa, inet_ntop 3 | from ssl import CERT_NONE 4 | from struct import unpack 5 | from sys import _getframe, exit 6 | from uuid import UUID 7 | 8 | import ldap3 9 | from Cryptodome.Cipher import AES 10 | from Cryptodome.Hash import MD4, SHA1 11 | from Cryptodome.Protocol.KDF import PBKDF2 12 | from ldap3 import ( 13 | ALL as LDAP3_ALL, 14 | ) 15 | from ldap3 import ( 16 | BASE, 17 | DEREF_NEVER, 18 | ENCRYPT, 19 | KERBEROS, 20 | MODIFY_REPLACE, 21 | NTLM, 22 | SASL, 23 | SIMPLE, 24 | SUBTREE, 25 | TLS_CHANNEL_BINDING, 26 | Connection, 27 | Server, 28 | ) 29 | from ldap3.core.exceptions import ( 30 | LDAPAttributeError, 31 | LDAPOperationResult, 32 | LDAPSocketOpenError, 33 | LDAPSocketSendError, 34 | ) 35 | from ldap3.extend.microsoft.addMembersToGroups import ( 36 | ad_add_members_to_groups as addUsersInGroups, 37 | ) 38 | from ldap3.extend.microsoft.modifyPassword import ad_modify_password 39 | from ldap3.extend.microsoft.removeMembersFromGroups import ( 40 | ad_remove_members_from_groups as removeUsersInGroups, 41 | ) 42 | from ldap3.extend.microsoft.unlockAccount import ad_unlock_account 43 | from ldap3.protocol.formatters.formatters import format_sid 44 | 45 | from ldeep.utils.sddl import parse_ntSecurityDescriptor 46 | from ldeep.utils.structure import Structure 47 | from ldeep.views.activedirectory import ( 48 | ALL, 49 | ActiveDirectoryView, 50 | validate_guid, 51 | validate_sid, 52 | ) 53 | from ldeep.views.constants import ( 54 | DNS_TYPES, 55 | GMSA_ENCRYPTION_CONSTANTS, 56 | LOGON_SAM_LOGON_RESPONSE_EX, 57 | PWD_PROPERTIES, 58 | SAM_ACCOUNT_TYPE, 59 | TRUSTS_INFOS, 60 | USER_ACCOUNT_CONTROL, 61 | WELL_KNOWN_SIDS, 62 | ) 63 | from ldeep.views.structures import MSDS_MANAGEDPASSWORD_BLOB 64 | 65 | 66 | # define an ldap3-compliant formatters 67 | def format_userAccountControl(raw_value): 68 | try: 69 | val = int(raw_value) 70 | result = [] 71 | for k, v in USER_ACCOUNT_CONTROL.items(): 72 | if v & val: 73 | result.append(k) 74 | return " | ".join(result) 75 | except (TypeError, ValueError): # expected exceptions↲ 76 | pass 77 | except ( 78 | Exception 79 | ): # any other exception should be investigated, anyway the formatters return the raw_value 80 | pass 81 | return raw_value 82 | 83 | 84 | # define an ldap3-compliant formatters 85 | def format_samAccountType(raw_value): 86 | try: 87 | val = int(raw_value) 88 | result = [] 89 | for k, v in SAM_ACCOUNT_TYPE.items(): 90 | if v & val: 91 | result.append(k) 92 | return " | ".join(result) 93 | except (TypeError, ValueError): # expected exceptions↲ 94 | pass 95 | except ( 96 | Exception 97 | ): # any other exception should be investigated, anyway the formatter returns the raw_value 98 | pass 99 | return raw_value 100 | 101 | 102 | # define an ldap3-compliant formatters 103 | def format_pwdProperties(raw_value): 104 | try: 105 | val = int(raw_value) 106 | result = [] 107 | for k, v in PWD_PROPERTIES.items(): 108 | if v & val: 109 | result.append(k) 110 | return " | ".join(result) 111 | except (TypeError, ValueError): # expected exceptions↲ 112 | pass 113 | except ( 114 | Exception 115 | ): # any other exception should be investigated, anyway the formatter returns the raw_value 116 | pass 117 | return raw_value 118 | 119 | 120 | # define an ldap3-compliant formatters 121 | def format_trustsInfos(raw_value): 122 | try: 123 | val = int(raw_value) 124 | result = [] 125 | for k, v in TRUSTS_INFOS.items(): 126 | if v & val: 127 | result.append(k) 128 | return " | ".join(result) 129 | except (TypeError, ValueError): # expected exceptions↲ 130 | pass 131 | except ( 132 | Exception 133 | ): # any other exception should be investigated, anyway the formatter returns the raw_value 134 | pass 135 | return raw_value 136 | 137 | 138 | # define an ldap3-compliant formatters 139 | def format_dnsrecord(raw_value): 140 | databytes = raw_value[0:4] 141 | datalen, datatype = unpack("HH", databytes) 142 | data = raw_value[24 : 24 + datalen] 143 | for recordname, recordvalue in DNS_TYPES.items(): 144 | if recordvalue == datatype: 145 | if recordname == "A": 146 | target = inet_ntoa(data) 147 | elif recordname == "AAAA": 148 | target = inet_ntop(AF_INET6, data) 149 | elif recordname == "NS": 150 | nbSegments = data[1] 151 | segments = [] 152 | index = 2 153 | for _ in range(nbSegments): 154 | segLen = data[index] 155 | segments.append(data[index + 1 : index + segLen + 1].decode()) 156 | index += segLen + 1 157 | target = ".".join(segments) 158 | else: 159 | return "" 160 | 161 | return "%s %s" % (recordname, target) 162 | 163 | 164 | def format_ad_timedelta(raw_value): 165 | """ 166 | Convert a negative filetime value to an integer timedelta. 167 | """ 168 | if isinstance(raw_value, bytes): 169 | raw_value = int(raw_value) 170 | return raw_value 171 | 172 | 173 | # from http://www.kouti.com/tables/baseattributes.htm 174 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.8"] = ( 175 | format_userAccountControl, 176 | None, 177 | ) 178 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.302"] = ( 179 | format_samAccountType, 180 | None, 181 | ) 182 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.382"] = ( 183 | format_dnsrecord, 184 | None, 185 | ) 186 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.121"] = ( 187 | format_sid, 188 | None, 189 | ) 190 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.93"] = ( 191 | format_pwdProperties, 192 | None, 193 | ) 194 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.60"] = ( 195 | format_ad_timedelta, 196 | None, 197 | ) 198 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.74"] = ( 199 | format_ad_timedelta, 200 | None, 201 | ) 202 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.78"] = ( 203 | format_ad_timedelta, 204 | None, 205 | ) 206 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.4.470"] = ( 207 | format_trustsInfos, 208 | None, 209 | ) 210 | ldap3.protocol.formatters.standard.standard_formatter["1.2.840.113556.1.2.281"] = ( 211 | parse_ntSecurityDescriptor, 212 | None, 213 | ) 214 | 215 | 216 | class LdapActiveDirectoryView(ActiveDirectoryView): 217 | """ 218 | Manage a LDAP connection to a LDAP Active Directory. 219 | """ 220 | 221 | # Constant functions 222 | USER_LOCKED_FILTER = ( 223 | lambda _: "(&(objectCategory=Person)(objectClass=user)(lockoutTime:1.2.840.113556.1.4.804:=4294967295))" 224 | ) 225 | GROUPS_FILTER = lambda _: "(objectClass=group)" 226 | ZONES_FILTER = lambda _: "(&(objectClass=dnsZone)(!(dc=RootDNSServers)))" 227 | ZONE_FILTER = lambda _: "(objectClass=dnsNode)" 228 | SITES_FILTER = lambda _: "(objectClass=site)" 229 | SUBNET_FILTER = lambda _, s: f"(SiteObject={s})" 230 | PKI_FILTER = lambda _: "(objectClass=pKIEnrollmentService)" 231 | TEMPLATE_FILTER = lambda _: "(objectClass=pKICertificateTemplate)" 232 | PRIMARY_SCCM_FILTER = lambda _: "(cn=System Management)" 233 | MP_SCCM_FILTER = lambda _: "(objectClass=mssmsmanagementpoint)" 234 | DP_SCCM_FILTER = lambda _: "(cn=*-Remote-Installation-Services)" 235 | USER_ALL_FILTER = lambda _: "(&(objectCategory=Person)(objectClass=user))" 236 | USER_SPN_FILTER = ( 237 | lambda _: "(&(objectCategory=Person)(objectClass=user)(servicePrincipalName=*)(!(sAMAccountName=krbtgt)))" 238 | ) 239 | USER_ACCOUNT_CONTROL_FILTER = ( 240 | lambda _, n: f"(&(objectCategory=Person)(objectClass=user)(userAccountControl:1.2.840.113556.1.4.803:={n}))" 241 | ) 242 | USER_ACCOUNT_CONTROL_FILTER_NEG = ( 243 | lambda _, n: f"(&(objectCategory=Person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:={n})))" 244 | ) 245 | ANR = lambda _, u: f"(anr={u})" 246 | DISTINGUISHED_NAME = lambda _, n: f"(distinguishedName={n})" 247 | COMPUTERS_FILTER = lambda _: "(objectClass=computer)" 248 | DC_FILTER = lambda _: "(userAccountControl:1.2.840.113556.1.4.803:=8192)" 249 | GROUP_DN_FILTER = lambda _, g: f"(&(objectClass=group)(sAMAccountName={g}))" 250 | USER_DN_FILTER = ( 251 | lambda _, u: f"(&(objectClass=user)(objectCategory=Person)(sAMAccountName={u}))" 252 | ) 253 | ACCOUNTS_IN_GROUP_FILTER = lambda _, p, g: f"(|(primaryGroupID={p})(memberOf={g}))" 254 | ACCOUNT_IN_GROUPS_FILTER = lambda _, u: f"(sAMAccountName={u})" 255 | PRIMARY_GROUP_ID = lambda s, i: f"(objectSid={s.get_domain_sid()}-{i})" 256 | DOMAIN_INFO_FILTER = lambda _: "(objectClass=domain)" 257 | GPO_INFO_FILTER = lambda _: "(objectCategory=groupPolicyContainer)" 258 | PSO_INFO_FILTER = lambda _: "(objectClass=msDS-PasswordSettings)" 259 | TRUSTS_INFO_FILTER = lambda _: "(objectCategory=trustedDomain)" 260 | OU_FILTER = lambda _: "(|(objectClass=OrganizationalUnit)(objectClass=domain))" 261 | ENUM_USER_FILTER = ( 262 | lambda _, n: f"(&(NtVer=\x06\x00\x00\x00)(AAC=\x10\x00\x00\x00)(User={n}))" 263 | ) 264 | ALL_FILTER = lambda _: "(objectClass=*)" 265 | AUTH_POLICIES_FILTER = lambda _: "(objectClass=msDS-AuthNPolicy)" 266 | SILOS_FILTER = lambda _: "(objectClass=msDS-AuthNPolicySilo)" 267 | SILO_FILTER = lambda _, s: f"(&(objectClass=msDS-AuthNPolicySilo)(cn={s}))" 268 | LAPS_FILTER = ( 269 | lambda _, s: f"(&(objectCategory=computer)(ms-Mcs-AdmPwdExpirationTime=*)(cn={s}))" 270 | ) 271 | LAPS2_FILTER = ( 272 | lambda _, s: f"(&(objectCategory=computer)(msLAPS-PasswordExpirationTime=*)(cn={s}))" 273 | ) 274 | GMSA_FILTER = ( 275 | lambda _, s: f"(&(ObjectClass=msDS-GroupManagedServiceAccount)(sAMAccountName={s}))" 276 | ) 277 | SMSA_FILTER = lambda _: "(ObjectClass=msDS-ManagedServiceAccount)" 278 | BITLOCKERKEY_FILTER = lambda _: "(objectClass=msFVE-RecoveryInformation)" 279 | FSMO_DOMAIN_NAMING_FILTER = ( 280 | lambda _: "(&(objectClass=crossRefContainer)(fSMORoleOwner=*))" 281 | ) 282 | FSMO_SCHEMA_FILTER = lambda _: "(&(objectClass=dMD)(fSMORoleOwner=*))" 283 | FSMO_DOMAIN_FILTER = lambda _: "(fSMORoleOwner=*)" 284 | SHADOW_PRINCIPALS_FILTER = lambda _: "(objectClass=msDS-ShadowPrincipal)" 285 | UNCONSTRAINED_DELEGATION_FILTER = ( 286 | lambda _: f"(userAccountControl:1.2.840.113556.1.4.803:=524288)" 287 | ) 288 | CONSTRAINED_DELEGATION_FILTER = lambda _: f"(msDS-AllowedToDelegateTo=*)" 289 | RESOURCE_BASED_CONSTRAINED_DELEGATION_FILTER = ( 290 | lambda _: f"(msDS-AllowedToActOnBehalfOfOtherIdentity=*)" 291 | ) 292 | ALL_DELEGATIONS_FILTER = ( 293 | lambda _: f"(|(userAccountControl:1.2.840.113556.1.4.803:=524288)(msDS-AllowedToDelegateTo=*)(msDS-AllowedToActOnBehalfOfOtherIdentity=*))" 294 | ) 295 | 296 | class ActiveDirectoryLdapException(Exception): 297 | pass 298 | 299 | def __init__( 300 | self, 301 | server, 302 | domain="", 303 | base="", 304 | forest_base="", 305 | username="", 306 | password="", 307 | ntlm="", 308 | pfx_file="", 309 | pfx_pass="", 310 | cert_pem="", 311 | key_pem="", 312 | method="NTLM", 313 | no_encryption=False, 314 | throttle=0, 315 | page_size=1000, 316 | ): 317 | """ 318 | LdapActiveDirectoryView constructor. 319 | Initialize the connection with the LDAP server. 320 | 321 | Three authentication modes: 322 | * Kerberos (ldap3 will automatically retrieve the $KRB5CCNAME env variable) 323 | * NTLM (username + NTLM hash/password) 324 | * SIMPLE (username + password) 325 | 326 | @server: Server to connect and perform LDAP query to. 327 | @domain: Fully qualified domain name of the Active Directory domain. 328 | @base: Base for the LDAP queries. 329 | @forest_base: Forest base for the LDAP queries. 330 | @username: Username to use for the authentication 331 | @password: Password to use for the authentication (for SIMPLE authentication) 332 | @ntlm: NTLM hash to use for the authentication (for NTLM authentication) 333 | @method: Either to use NTLM, SIMPLE, Kerberos or anonymous authentication. 334 | @no_encryption: Either the communication is encrypted or not. 335 | 336 | @throw ActiveDirectoryLdapException when the connection or the bind does not work. 337 | """ 338 | self.username = username 339 | self.password = password 340 | self.ntlm = ntlm 341 | self.pfx_file = pfx_file 342 | self.pfx_pass = pfx_pass 343 | self.cert = cert_pem 344 | self.key = key_pem 345 | self.no_encryption = no_encryption 346 | self.server = server 347 | self.domain = domain 348 | self.hostnames = [] 349 | self.throttle = throttle 350 | self.page_size = page_size 351 | 352 | self.set_controls() 353 | self.set_all_attributes() 354 | 355 | if method == "Certificate": 356 | if not self.server.startswith("ldaps"): 357 | # TODO start tls if ldap 358 | print( 359 | "At this moment ldeep needs to use ldaps (use ldaps:// before the server parameter)" 360 | ) 361 | exit(1) 362 | else: 363 | if self.pfx_file: 364 | from cryptography.hazmat.primitives import serialization 365 | from cryptography.hazmat.primitives.serialization import pkcs12 366 | 367 | with open(pfx_file, "rb") as f: 368 | pfxdata = f.read() 369 | if self.pfx_pass: 370 | from oscrypto.asymmetric import ( 371 | dump_certificate, 372 | dump_openssl_private_key, 373 | load_private_key, 374 | rsa_pkcs1v15_sign, 375 | ) 376 | from oscrypto.keys import ( 377 | parse_certificate, 378 | parse_pkcs12, 379 | parse_private, 380 | ) 381 | 382 | if isinstance(self.pfx_pass, str): 383 | pfxpass = self.pfx_pass.encode() 384 | privkeyinfo, certinfo, _ = parse_pkcs12( 385 | pfxdata, password=pfxpass 386 | ) 387 | key = dump_openssl_private_key(privkeyinfo, self.pfx_pass) 388 | cert = dump_certificate(certinfo, encoding="pem") 389 | else: 390 | privkey, cert, extra_certs = pkcs12.load_key_and_certificates( 391 | pfxdata, None 392 | ) 393 | key = privkey.private_bytes( 394 | encoding=serialization.Encoding.PEM, 395 | format=serialization.PrivateFormat.TraditionalOpenSSL, 396 | encryption_algorithm=serialization.NoEncryption(), 397 | ) 398 | cert = cert.public_bytes(encoding=serialization.Encoding.PEM) 399 | try: 400 | from tempfile import gettempdir 401 | 402 | key_path = f"{gettempdir()}/ldeep_key" 403 | cert_path = f"{gettempdir()}/ldeep_cert" 404 | with open(key_path, "wb") as f1, open(cert_path, "wb") as f2: 405 | f1.write(key) 406 | f2.write(cert) 407 | except PermissionError: 408 | print("Can't write key and cert to disk") 409 | exit(1) 410 | tls = ldap3.Tls( 411 | local_private_key_file=key_path, 412 | local_certificate_file=cert_path, 413 | validate=CERT_NONE, 414 | ) 415 | else: 416 | tls = ldap3.Tls( 417 | local_private_key_file=self.key, 418 | local_certificate_file=self.cert, 419 | validate=CERT_NONE, 420 | ) 421 | else: 422 | tls = ldap3.Tls(validate=CERT_NONE) 423 | 424 | if self.server.startswith("ldaps"): 425 | server = Server( 426 | self.server, 427 | port=636, 428 | use_ssl=True, 429 | allowed_referral_hosts=[("*", True)], 430 | get_info=LDAP3_ALL, 431 | tls=tls, 432 | ) 433 | else: 434 | server = Server(self.server, get_info=LDAP3_ALL) 435 | 436 | if method == "Kerberos": 437 | if self.server.startswith("ldaps"): 438 | self.ldap = Connection( 439 | server, authentication=SASL, sasl_mechanism=KERBEROS 440 | ) 441 | else: 442 | if self.no_encryption: 443 | self.ldap = Connection( 444 | server, 445 | authentication=SASL, 446 | sasl_mechanism=KERBEROS, 447 | ) 448 | else: 449 | self.ldap = Connection( 450 | server, 451 | authentication=SASL, 452 | sasl_mechanism=KERBEROS, 453 | session_security=ENCRYPT, 454 | ) 455 | elif method == "Certificate": 456 | self.ldap = Connection(server) 457 | elif method == "anonymous": 458 | self.ldap = Connection(server) 459 | elif method == "NTLM": 460 | if password is not None: 461 | ntlm = password 462 | else: 463 | try: 464 | lm, nt = ntlm.split(":") 465 | lm = "aad3b435b51404eeaad3b435b51404ee" if not lm else lm 466 | ntlm = f"{lm}:{nt}" 467 | except Exception as e: 468 | print(e) 469 | print("Incorrect hash, format is LMHASH:NTHASH") 470 | exit(1) 471 | if self.server.startswith("ldaps"): 472 | if self.no_encryption: 473 | self.ldap = Connection( 474 | server, 475 | user=f"{domain}\\{username}", 476 | password=ntlm, 477 | authentication=NTLM, 478 | check_names=True, 479 | ) 480 | else: 481 | self.ldap = Connection( 482 | server, 483 | user=f"{domain}\\{username}", 484 | password=ntlm, 485 | authentication=NTLM, 486 | channel_binding=TLS_CHANNEL_BINDING, 487 | check_names=True, 488 | ) 489 | else: 490 | if self.no_encryption: 491 | self.ldap = Connection( 492 | server, 493 | user=f"{domain}\\{username}", 494 | password=ntlm, 495 | authentication=NTLM, 496 | check_names=True, 497 | ) 498 | else: 499 | self.ldap = Connection( 500 | server, 501 | user=f"{domain}\\{username}", 502 | password=ntlm, 503 | authentication=NTLM, 504 | session_security=ENCRYPT, 505 | check_names=True, 506 | ) 507 | elif method == "SIMPLE": 508 | if "." in domain: 509 | domain, _, _ = domain.partition(".") 510 | if not password: 511 | print("Password is required with simple bind (-p)") 512 | exit(1) 513 | self.ldap = Connection( 514 | server, 515 | user=f"{domain}\\{username}", 516 | password=password, 517 | authentication=SIMPLE, 518 | check_names=True, 519 | ) 520 | 521 | try: 522 | if method == "Certificate": 523 | import os 524 | 525 | try: 526 | self.ldap.open() 527 | if self.pfx_file: 528 | os.remove(key_path) 529 | os.remove(cert_path) 530 | except LDAPSocketOpenError: 531 | print( 532 | "Cannot get private key data, corrupted key or wrong passphrase ?" 533 | ) 534 | if self.pfx_file: 535 | os.remove(key_path) 536 | os.remove(cert_path) 537 | exit(1) 538 | except Exception as e: 539 | print(f"Unhandled Exception: {e}") 540 | import traceback 541 | 542 | traceback.print_exc() 543 | exit(1) 544 | else: 545 | if not self.ldap.bind(): 546 | raise self.ActiveDirectoryLdapException( 547 | f"Unable to bind to the LDAP server: {self.ldap.result['description']} ({self.ldap.result['message']})" 548 | ) 549 | if method == "anonymous": 550 | anon_base = self.ldap.request["base"].split(",") 551 | for i, item in enumerate(anon_base): 552 | if item.startswith("DC="): 553 | anon_base = ",".join(anon_base[i:]) 554 | break 555 | self.ldap.search( 556 | search_base=anon_base, 557 | search_filter="(objectClass=*)", 558 | search_scope="SUBTREE", 559 | attributes="*", 560 | ) 561 | 562 | if len(self.ldap.entries) == 0: 563 | raise self.ActiveDirectoryLdapException( 564 | "Unable to retrieve information with anonymous bind" 565 | ) 566 | except LDAPSocketOpenError: 567 | raise self.ActiveDirectoryLdapException( 568 | f"Unable to open connection with {self.server}" 569 | ) 570 | except LDAPSocketSendError: 571 | raise self.ActiveDirectoryLdapException( 572 | f"Unable to open connection with {self.server}, maybe LDAPS is not enabled ?" 573 | ) 574 | 575 | self.base_dn = base or server.info.other["defaultNamingContext"][0] 576 | self.forest_base_dn = ( 577 | forest_base or server.info.other["rootDomainNamingContext"][0] 578 | ) 579 | self.fqdn = ".".join( 580 | map( 581 | lambda x: x.replace("DC=", ""), 582 | filter(lambda x: x.startswith("DC="), self.base_dn.split(",")), 583 | ) 584 | ) 585 | self.search_scope = SUBTREE 586 | 587 | def set_controls(self, controls=[]): 588 | self.controls = controls 589 | 590 | def set_all_attributes(self, attributes=ALL): 591 | self.attributes = attributes 592 | 593 | def all_attributes(self): 594 | return self.attributes 595 | 596 | # Not used anymore 597 | def __query(self, ldapfilter, attributes=[], base=None, scope=None): 598 | """ 599 | Perform a query to the LDAP server and return the results. 600 | 601 | @ldapfilter: The LDAP filter to query (see RFC 2254). 602 | @attributes: List of attributes to retrieved with the query. 603 | @base: Base to use during the request. 604 | @scope: Scope to use during the request. 605 | 606 | @return a list of records. 607 | """ 608 | attributes = self.attributes if attributes == [] else attributes 609 | result_set = [] 610 | try: 611 | entry_generator = self.ldap.extend.standard.paged_search( 612 | search_base=base or self.base_dn, 613 | search_filter=ldapfilter, 614 | search_scope=scope or self.search_scope, 615 | attributes=attributes, 616 | controls=self.controls, 617 | paged_size=self.page_size, 618 | generator=True, 619 | ) 620 | 621 | for entry in entry_generator: 622 | if "dn" in entry: 623 | d = entry["attributes"] 624 | d["dn"] = entry["dn"] 625 | result_set.append(d) 626 | 627 | except LDAPOperationResult as e: 628 | raise self.ActiveDirectoryLdapException(e) 629 | except LDAPAttributeError as e: 630 | if not _getframe().f_back.f_code.co_name == "get_laps": 631 | raise self.ActiveDirectoryLdapException(e) 632 | 633 | return result_set 634 | 635 | def query(self, ldapfilter, attributes=[], base=None, scope=None): 636 | """ 637 | Perform a query to the LDAP server and return the results as a generator. 638 | 639 | @ldapfilter: The LDAP filter to query (see RFC 2254). 640 | @attributes: List of attributes to retrieved with the query. 641 | @base: Base to use during the request. 642 | @scope: Scope to use during the request. 643 | 644 | @return a generator yielding records. 645 | """ 646 | attributes = self.attributes if attributes == [] else attributes 647 | # result_set = [] 648 | try: 649 | entry_generator = self.ldap.extend.standard.paged_search( 650 | search_base=base or self.base_dn, 651 | search_filter=ldapfilter, 652 | search_scope=scope or self.search_scope, 653 | attributes=attributes, 654 | controls=self.controls, 655 | paged_size=self.page_size, 656 | generator=True, 657 | ) 658 | 659 | except LDAPOperationResult as e: 660 | raise self.ActiveDirectoryLdapException(e) 661 | except LDAPAttributeError as e: 662 | if not _getframe().f_back.f_code.co_name == "get_laps": 663 | raise self.ActiveDirectoryLdapException(e) 664 | 665 | def result(x): 666 | if "dn" in x: 667 | d = x["attributes"] 668 | d["dn"] = x["dn"] 669 | return dict(d) 670 | 671 | return filter(lambda x: x is not None, map(result, entry_generator)) 672 | 673 | def query_server_info(self): 674 | return [json_loads(self.ldap.server.info.to_json())] 675 | 676 | def create_objecttype_guid_map(self): 677 | self.objecttype_guid_map = dict() 678 | sresult = self.ldap.extend.standard.paged_search( 679 | self.ldap.server.info.other["schemaNamingContext"][0], 680 | "(objectClass=*)", 681 | attributes=["name", "schemaidguid"], 682 | ) 683 | for res in sresult: 684 | if res["attributes"]["schemaIDGUID"]: 685 | guid = str(UUID(bytes_le=res["attributes"]["schemaIDGUID"])) 686 | self.objecttype_guid_map[res["attributes"]["name"].lower()] = guid 687 | 688 | def get_domain_sid(self): 689 | """ 690 | Return the current domain SID by issuing a LDAP request. 691 | 692 | @return the domain sid or None if a problem occurred. 693 | """ 694 | results = list(self.query(self.DOMAIN_INFO_FILTER(), ["ObjectSid"])) 695 | 696 | if results: 697 | return results[0]["objectSid"] 698 | 699 | return None 700 | 701 | def resolve_sid(self, sid): 702 | """ 703 | Two cases: 704 | * the SID is a WELL KNOWN SID and a local SID, the name of the corresponding account is returned; 705 | * else, the SID is search through the LDAP and the corresponding record is returned. 706 | 707 | @sid: the sid to search for. 708 | 709 | @throw ActiveDirectoryInvalidSID if the SID is not a valid SID. 710 | @return the record corresponding to the SID queried. 711 | """ 712 | if sid in WELL_KNOWN_SIDS: 713 | return WELL_KNOWN_SIDS[sid] 714 | elif validate_sid(sid): 715 | results = self.query(f"(&(ObjectSid={sid}))") 716 | if results: 717 | return results 718 | raise self.ActiveDirectoryInvalidSID(f"SID: {sid}") 719 | 720 | def resolve_guid(self, guid): 721 | """ 722 | Return the LDAP record with the provided GUID. 723 | 724 | @guid: the guid to search for. 725 | 726 | @throw ActiveDirectoryInvalidGUID if the GUID is not a valid GUID. 727 | @return the record corresponding to the guid queried. 728 | """ 729 | if validate_guid(guid): 730 | results = self.query(f"(&(ObjectGUID={guid}))") 731 | # Normally only one result should have been retrieved: 732 | if results: 733 | return results 734 | raise self.ActiveDirectoryInvalidGUID(f"GUID: {guid}") 735 | 736 | def get_sddl(self, ldapfilter, base=None, scope=None): 737 | """ 738 | Perform a query to the LDAP server and return the results. 739 | 740 | @ldapfiler: The LDAP filter to query (see RFC 2254). 741 | @attributes: List of attributes to retrieved with the query. 742 | @base: Base to use during the request. 743 | @scope: Scope to use during the request. 744 | 745 | @return a list of records. 746 | """ 747 | result_set = [] 748 | try: 749 | result = self.ldap.search( 750 | search_base=base or self.base_dn, 751 | search_filter=ldapfilter, 752 | search_scope=scope or self.search_scope, 753 | attributes=["ntSecurityDescriptor"], 754 | controls=[("1.2.840.113556.1.4.801", True, b"\x30\x03\x02\x01\x07")], 755 | ) 756 | 757 | if not result: 758 | raise self.ActiveDirectoryLdapException() 759 | else: 760 | for entry in self.ldap.response: 761 | if "dn" in entry: 762 | d = entry["attributes"] 763 | d["dn"] = entry["dn"] 764 | result_set.append(dict(d)) 765 | 766 | except LDAPOperationResult as e: 767 | raise self.ActiveDirectoryLdapException(e) 768 | 769 | return result_set 770 | 771 | def get_gmsa(self, attributes, target): 772 | entries = list(self.query(self.GMSA_FILTER(target), attributes)) 773 | 774 | constants = GMSA_ENCRYPTION_CONSTANTS 775 | iv = b"\x00" * 16 776 | 777 | for entry in entries: 778 | sam = entry["sAMAccountName"] 779 | data = entry["msDS-ManagedPassword"] 780 | try: 781 | readers = entry["msDS-GroupMSAMembership"] 782 | except Exception: 783 | readers = [] 784 | # Find principals who can read the password 785 | if readers: 786 | try: 787 | readers_sd = parse_ntSecurityDescriptor(readers) 788 | entry["readers"] = [] 789 | for ace in readers_sd["DACL"]["ACEs"]: 790 | try: 791 | reader_object = list(self.resolve_sid(ace["SID"])) 792 | if reader_object: 793 | name = reader_object[0]["sAMAccountName"] 794 | if "group" in reader_object[0]["objectClass"]: 795 | name += " (group)" 796 | entry["readers"].append(name) 797 | else: 798 | entry["readers"].append(ace["SID"]) 799 | except Exception: 800 | pass 801 | except Exception: 802 | pass 803 | blob = MSDS_MANAGEDPASSWORD_BLOB() 804 | try: 805 | blob.fromString(data) 806 | except (TypeError, KeyError): 807 | continue 808 | 809 | password = blob["CurrentPassword"][:-2] 810 | 811 | # Compute NT hash 812 | hash = MD4.new() 813 | hash.update(password) 814 | nthash = hash.hexdigest() 815 | 816 | # Quick and dirty way to get the FQDN of the account's domain 817 | dc_list = [] 818 | for s in entry["dn"].split(","): 819 | if s.startswith("DC="): 820 | dc_list.append(s[3:]) 821 | 822 | domain_fqdn = ".".join(dc_list) 823 | salt = f"{domain_fqdn.upper()}host{sam[:-1].lower()}.{domain_fqdn.lower()}" 824 | encryption_key = PBKDF2( 825 | password.decode("utf-16-le", "replace").encode(), 826 | salt.encode(), 827 | 32, 828 | count=4096, 829 | hmac_hash_module=SHA1, 830 | ) 831 | 832 | # Compute AES keys 833 | cipher = AES.new(encryption_key, AES.MODE_CBC, iv) 834 | first_part = cipher.encrypt(constants) 835 | cipher = AES.new(encryption_key, AES.MODE_CBC, iv) 836 | second_part = cipher.encrypt(first_part) 837 | aes256_key = first_part[:16] + second_part[:16] 838 | 839 | cipher = AES.new(encryption_key[:16], AES.MODE_CBC, iv) 840 | aes128_key = cipher.encrypt(constants[:16]) 841 | 842 | entry["nthash"] = f"{nthash}" 843 | entry["aes128-cts-hmac-sha1-96"] = f"{aes128_key.hex()}" 844 | entry["aes256-cts-hmac-sha1-96"] = f"{aes256_key.hex()}" 845 | 846 | return entries 847 | 848 | def unlock(self, username): 849 | """ 850 | Unlock an account. 851 | 852 | @username: the username associated to the account to unlock. 853 | 854 | @throw ActiveDirectoryLdapException if the account does not exist or the query returns more than one result. 855 | @return True if the account was successfully unlock or False otherwise. 856 | """ 857 | results = list(self.query(self.USER_DN_FILTER(username))) 858 | if len(results) != 1: 859 | raise self.ActiveDirectoryLdapException("Zero or non uniq result") 860 | else: 861 | user = results[0] 862 | unlock = ad_unlock_account(self.ldap, user["dn"]) 863 | # goddamn, return value is either True or str... 864 | return isinstance(unlock, bool) 865 | 866 | def modify_password(self, username, oldpassword, newpassword): 867 | """ 868 | Change the password of `username`. 869 | 870 | @username: the username associated to the account to modify its password. 871 | @newpassword: the new password to apply. 872 | @oldpassword: the old password. 873 | 874 | @throw ActiveDirectoryLdapException if the account does not exist or the query returns more than one result. 875 | @return True if the account was successfully unlock or False otherwise. 876 | """ 877 | results = list(self.query(self.USER_DN_FILTER(username))) 878 | if len(results) != 1: 879 | raise self.ActiveDirectoryLdapException("Zero or non uniq result") 880 | else: 881 | user = results[0] 882 | res = ad_modify_password( 883 | self.ldap, user["dn"], newpassword, old_password=oldpassword 884 | ) 885 | if res == False: 886 | res = ad_modify_password( 887 | self.ldap, user["dn"], newpassword, old_password=None 888 | ) 889 | return res 890 | 891 | def add_user_to_group(self, user_dn, group_dn): 892 | """ 893 | Add user to a group. 894 | 895 | @username: the username that will be added to the group. DN format: "CN=username,CN=Users,DC=CORP,DC=LOCAL" 896 | @group: the target group. DN format: "CN=group,CN=Users,DC=CORP,DC=LOCAL" 897 | 898 | @return True if the account was successfully added or False otherwise. 899 | """ 900 | try: 901 | return addUsersInGroups(self.ldap, user_dn, group_dn) 902 | except ldap3.core.exceptions.LDAPInvalidDnError as e: 903 | print(f"Unhandled exception: {e}") 904 | # catch invalid group dn 905 | return False 906 | 907 | def remove_user_from_group(self, user_dn, group_dn): 908 | """ 909 | Remove user from a group. 910 | 911 | @username: the username that will be removed from the group. dn format: "CN=username,CN=Users,DC=CORP,DC=LOCAL" 912 | @group: the target group. dn format: "CN=group,CN=Users,DC=CORP,DC=LOCAL" 913 | 914 | @return True if the account was successfully removed or if the account doesn't exist or False otherwise. 915 | """ 916 | try: 917 | return removeUsersInGroups(self.ldap, user_dn, group_dn, fix=True) 918 | except ldap3.core.exceptions.LDAPInvalidDnError as e: 919 | print(f"Unhandled exception: {e}") 920 | # catch invalid group dn 921 | return False 922 | 923 | def change_uac(self, user_dn, uac): 924 | """ 925 | Change userAccountControl. 926 | 927 | @username: the target user of UAC change 928 | @uac: the integer value for the userAccountControl. Ex: 512 for NORMAL_ACCOUNT 929 | 930 | @return True if the UAC was successfully changed or False otherwise. 931 | """ 932 | try: 933 | return self.ldap.modify( 934 | user_dn, {"userAccountControl": [(MODIFY_REPLACE, [uac])]} 935 | ) 936 | except ldap3.core.exceptions.LDAPInvalidDnError as e: 937 | print(f"Unhandled exception: {e}") 938 | # catch invalid group dn 939 | return False 940 | 941 | def user_exists(self, username): 942 | """ 943 | Perform an LDAP ping to determine if the specified user exists. 944 | 945 | @username: the username to test. 946 | 947 | @return True if the user exists, False otherwise. 948 | """ 949 | try: 950 | result = self.ldap.search( 951 | "", 952 | search_filter=self.ENUM_USER_FILTER(username), 953 | search_scope=BASE, 954 | attributes=["NetLogon"], 955 | dereference_aliases=DEREF_NEVER, 956 | ) 957 | 958 | if not result: 959 | raise self.ActiveDirectoryLdapException() 960 | else: 961 | for entry in self.ldap.response: 962 | attr = entry.get("raw_attributes") 963 | if attr: 964 | netlogon = attr.get("netlogon") 965 | if ( 966 | netlogon 967 | and len(netlogon[0]) > 1 968 | and netlogon[0][:2] == LOGON_SAM_LOGON_RESPONSE_EX 969 | ): 970 | return True 971 | 972 | except LDAPOperationResult as e: 973 | raise self.ActiveDirectoryLdapException(e) 974 | 975 | return False 976 | 977 | def create_computer(self, computer, password): 978 | """ 979 | Create a computer account on the domain. 980 | 981 | @computer: the name of the create computer. 982 | @password: the password of the computer to create. 983 | 984 | @return the result code on the add action 985 | """ 986 | computer_dn = f"CN={computer},CN=Computers,{self.base_dn}" 987 | domain = ".".join(part.split("=")[1] for part in self.base_dn.split(",")) 988 | # Default computer SPNs 989 | spns = [ 990 | f"HOST/{computer.strip('$')}", 991 | f"HOST/{computer.strip('$')}.{domain}", 992 | f"RestrictedKrbHost/{computer.strip('$')}", 993 | f"RestrictedKrbHost/{computer.strip('$')}.{domain}", 994 | ] 995 | ucd = { 996 | "dnsHostName": "%s.%s" % (computer.strip("$"), domain), 997 | "userAccountControl": 0x1000, # WORKSTATION_TRUST_ACCOUNT 998 | "servicePrincipalName": spns, 999 | "sAMAccountName": computer, 1000 | "unicodePwd": ('"%s"' % password).encode("utf-16-le"), 1001 | } 1002 | try: 1003 | result = self.ldap.add( 1004 | computer_dn, 1005 | ["top", "person", "organizationalPerson", "user", "computer"], 1006 | ucd, 1007 | ) 1008 | except Exception as e: 1009 | raise self.ActiveDirectoryLdapException(e) 1010 | return result 1011 | 1012 | def create_user(self, user, password): 1013 | """ 1014 | Create a user account on the domain. 1015 | 1016 | @user: the name of the create user. 1017 | @password: the password of the user to create. 1018 | 1019 | @return the result code on the add action 1020 | """ 1021 | user_dn = f"CN={user},CN=Users,{self.base_dn}" 1022 | 1023 | ucd = { 1024 | "objectCategory": "CN=Person,CN=Schema,CN=Configuration,%s" % self.base_dn, 1025 | "distinguishedName": user_dn, 1026 | "cn": user, 1027 | "sn": user, 1028 | "givenName": user, 1029 | "displayName": user, 1030 | "name": user, 1031 | "userAccountControl": 0x200, # NORMAL_ACCOUNT (decimal value: 512) 1032 | "accountExpires": 0, 1033 | "sAMAccountName": user, 1034 | "unicodePwd": ('"%s"' % password).encode("utf-16-le"), 1035 | } 1036 | try: 1037 | result = self.ldap.add( 1038 | user_dn, ["top", "person", "organizationalPerson", "user"], ucd 1039 | ) 1040 | except Exception as e: 1041 | raise self.ActiveDirectoryLdapException(e) 1042 | return result 1043 | -------------------------------------------------------------------------------- /ldeep/views/structures.py: -------------------------------------------------------------------------------- 1 | from ldeep.utils.structure import Structure 2 | 3 | 4 | class MSDS_MANAGEDPASSWORD_BLOB(Structure): 5 | structure = ( 6 | ("Version", "= 1.1.1, < 2", 11 | "cryptography>=42.0.7", 12 | "dnspython >= 1.15.0", 13 | "gssapi >= 1.8.0, < 2", 14 | "ldap3-bleeding-edge == 2.10.1.1337", 15 | "oscrypto >= 1.3.0, < 2", 16 | "pycryptodome >= 3.19.0, < 4", 17 | "pycryptodomex >= 3.19.0, < 4", 18 | "six >= 1.16.0, < 2", 19 | "termcolor >= 2.3.0, < 3", 20 | "tqdm >= 4.26.0, < 5", 21 | ] 22 | requires-python = ">=3.8.1,<3.14" 23 | readme = "README.rst" 24 | license = {text = "MIT"} 25 | keywords = [ 26 | "pentesting security windows active-directory networks", 27 | ] 28 | classifiers = [ 29 | "Development Status :: 4 - Beta", 30 | "Intended Audience :: Information Technology", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Programming Language :: Python :: 3 :: Only", 35 | "Programming Language :: Python :: 3.8", 36 | "Topic :: Security", 37 | ] 38 | 39 | [tool.pdm.version] 40 | source = "scm" 41 | write_to = "ldeep/_version.py" 42 | write_template = "__version__ = '{}'" 43 | 44 | [project.urls] 45 | Homepage = "https://github.com/franc-pentest/ldeep" 46 | 47 | [project.scripts] 48 | ldeep = "ldeep.__main__:main" 49 | 50 | [build-system] 51 | requires = ["pdm-backend"] 52 | build-backend = "pdm.backend" 53 | 54 | [dependency-groups] 55 | dev = [ 56 | "nuitka>=2.7", 57 | "ruff>=0.11.7", 58 | "twine>=6.1.0", 59 | "black>=24.8.0", 60 | ] 61 | --------------------------------------------------------------------------------