├── .github └── workflows │ └── python-windows-exe.yml ├── .gitignore ├── .readthedocs.yml ├── .vscode └── settings.json ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── builder └── pyinstaller │ └── build.bat ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── api.rst │ ├── client.rst │ ├── conf.py │ ├── connection.rst │ ├── credential.rst │ ├── factory.rst │ ├── index.rst │ ├── proxy.rst │ ├── target.rst │ ├── tutorial.rst │ └── urltutorial.rst ├── examples └── client.py ├── msldap ├── __init__.py ├── __main__.py ├── _version.py ├── bloodhound.py ├── client.py ├── commons │ ├── __init__.py │ ├── adexplorer.py │ ├── authbuilder.py │ ├── common.py │ ├── exceptions.py │ ├── factory.py │ ├── ldif.py │ ├── plugin.py │ ├── target.py │ └── utils.py ├── connection.py ├── dontuse │ └── test.py ├── examples │ ├── __init__.py │ ├── jsontest.py │ ├── msldapbloodhound.py │ ├── msldapclient.py │ ├── msldapcompdnslist.py │ └── utils │ │ ├── __init__.py │ │ └── completers.py ├── external │ ├── Readme.md │ ├── __init__.py │ ├── aiocmd │ │ ├── LICENSE │ │ ├── README.md │ │ ├── __init__.py │ │ ├── aiocmd │ │ │ ├── __init__.py │ │ │ ├── aiocmd.py │ │ │ └── nested_completer.py │ │ └── docs │ │ │ ├── example.py │ │ │ ├── image1.png │ │ │ └── image2.png │ ├── asciitree │ │ ├── LICENSE │ │ ├── MANIFEST.in │ │ ├── PKG-INFO │ │ ├── README.rst │ │ ├── __init__.py │ │ ├── asciitree │ │ │ ├── __init__.py │ │ │ ├── drawing.py │ │ │ ├── traversal.py │ │ │ └── util.py │ │ ├── setup.cfg │ │ └── setup.py │ └── bloodhoundpy │ │ ├── __init__.py │ │ ├── acls.py │ │ ├── lib │ │ ├── __init__.py │ │ └── cstruct.py │ │ ├── resolver.py │ │ └── utils.py ├── ldap_objects │ ├── __init__.py │ ├── adca.py │ ├── adcertificatetemplate.py │ ├── adcomp.py │ ├── adcontainer.py │ ├── addmsa.py │ ├── adenrollmentservice.py │ ├── adgmsa.py │ ├── adgpo.py │ ├── adgroup.py │ ├── adinfo.py │ ├── adou.py │ ├── adschemaentry.py │ ├── adsec.py │ ├── adtrust.py │ ├── aduser.py │ └── common.py ├── network │ ├── __init__.py │ └── packetizer.py ├── protocol │ ├── __init__.py │ ├── constants.py │ ├── ldap_filter │ │ ├── __init__.py │ │ ├── filter.py │ │ ├── parser.py │ │ └── soundex.py │ ├── messages.py │ ├── query.py │ ├── typeconversion.py │ └── utils.py ├── relay │ ├── __init__.py │ ├── server.py │ └── serverconnection.py └── wintypes │ ├── __init__.py │ ├── asn1 │ ├── __init__.py │ └── sdflagsrequest.py │ ├── dnsp │ ├── __init__.py │ └── structures │ │ ├── __init__.py │ │ ├── dnsproperty.py │ │ ├── dnsrecord.py │ │ └── misc.py │ ├── encryptedlaps.py │ └── managedpassword.py ├── pyproject.toml └── setup.py /.github/workflows/python-windows-exe.yml: -------------------------------------------------------------------------------- 1 | name: Build Windows Executable - PyInstaller 2 | # Description: 3 | # Most of my projects come with a build.bat script that uses PyInstaller to freeze the examples 4 | # to an executable file. This Action will set up the envrionment and run this build.bat script, 5 | # then upload the resulting executables to a google cloud bucket. 6 | # Additionally the executables will be compressed and encrypted using 7z 7 | 8 | on: 9 | push: 10 | branches: 11 | - main # Trigger on push to master branch 12 | 13 | jobs: 14 | build: 15 | runs-on: windows-latest # Use a Windows runner 16 | permissions: 17 | contents: 'read' 18 | id-token: 'write' 19 | 20 | steps: 21 | - uses: 'actions/checkout@v4' 22 | with: 23 | fetch-depth: 0 24 | - id: 'auth' 25 | uses: 'google-github-actions/auth@v1' 26 | with: 27 | credentials_json: '${{ secrets.GCLOUD_BUCKET_SERVICE_USER_SECRET }}' 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v2 31 | with: 32 | python-version: '3.9' 33 | 34 | - name: Install Dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install pyinstaller virtualenv 38 | 39 | - name: Run Batch File to Build Executable 40 | run: builder\pyinstaller\build.bat 41 | 42 | - name: Compress executables 43 | run: | 44 | "C:\Program Files\7-Zip\7z.exe" a secure.7z *.exe -pprotected 45 | working-directory: ${{ github.workspace }}/builder/pyinstaller 46 | shell: cmd 47 | 48 | #- name: Upload Executable 49 | # uses: actions/upload-artifact@v2 50 | # with: 51 | # name: executable 52 | # path: builder\pyinstaller\*.exe 53 | 54 | - name: 'Set up Cloud SDK' 55 | uses: 'google-github-actions/setup-gcloud@v1' 56 | with: 57 | version: '>= 390.0.0' 58 | 59 | - name: Upload Executables to GCS 60 | run: | 61 | $PROJVERSION = python -c "import sys; sys.path.append('${{ github.event.repository.name }}'); import _version; print(_version.__version__)" 62 | Write-Host "Detected Version: $PROJVERSION" 63 | gsutil cp builder\pyinstaller\*.exe gs://skelsec-github-foss/${{ github.event.repository.name }}/$PROJVERSION/ 64 | gsutil cp builder\pyinstaller\*.7z gs://skelsec-github-foss/${{ github.event.repository.name }}/$PROJVERSION/ 65 | shell: powershell 66 | 67 | - uses: sarisia/actions-status-discord@v1 68 | if: always() 69 | with: 70 | webhook: ${{ secrets.DISCORD_WEBHOOK }} 71 | status: ${{ job.status }} 72 | content: | 73 | ${{ github.event_name == 'push' && format('Hey all! A new commit was pushed to {0}!', github.repository) || '' }} 74 | ${{ github.event_name == 'pull_request' && format('Hey all! A new pull request has been opened on {0}!', github.repository) || '' }} 75 | ${{ github.event_name == 'release' && format('Hey all! A new release was created for project {0}!', github.event.repository.name) || '' }} 76 | title: | 77 | ${{ github.event_name == 'push' && 'Push Notification' || '' }} 78 | ${{ github.event_name == 'pull_request' && 'Pull Request Notification' || '' }} 79 | ${{ github.event_name == 'release' && 'Release Notification' || '' }} 80 | color: | 81 | ${{ github.event_name == 'push' && '0x00ff00' || '' }} 82 | ${{ github.event_name == 'pull_request' && '0xff0000' || '' }} 83 | ${{ github.event_name == 'release' && '0x0000ff' || '' }} 84 | url: "${{ github.server_url }}/${{ github.repository }}" 85 | username: GitHub Actions 86 | avatar_url: "https://avatars.githubusercontent.com/u/19204702" 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | *.exe 106 | *.json 107 | !msldap/external/bloodhoundpy/* -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | build: 2 | image: latest 3 | 4 | python: 5 | version: 3.7 6 | 7 | requirements_file: docs/requirements.txt -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "restructuredtext.confPath": "${workspaceFolder}/docs/source", 3 | "cSpell.ignoreWords": [ 4 | "dont", 5 | "preauth", 6 | "req", 7 | "rtype" 8 | ] 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This projects contains two other project written by a 3rd party. 2 | All code is licensed under MIT. 3 | 4 | License for MSLDAP: 5 | MIT License 6 | 7 | Copyright (c) 2018 Tamas Jos (@skelsec) 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | 27 | 28 | License for "asciitree": 29 | 30 | Copyright (c) 2015 Marc Brinkmann 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a 33 | copy of this software and associated documentation files (the "Software"), 34 | to deal in the Software without restriction, including without limitation 35 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 36 | and/or sell copies of the Software, and to permit persons to whom the 37 | Software is furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in 40 | all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 47 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 48 | DEALINGS IN THE SOFTWARE. 49 | 50 | 51 | License for "aiocmd": 52 | 53 | MIT License 54 | 55 | Copyright (c) 2019 Dor Green 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining a copy 58 | of this software and associated documentation files (the "Software"), to deal 59 | in the Software without restriction, including without limitation the rights 60 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | copies of the Software, and to permit persons to whom the Software is 62 | furnished to do so, subject to the following conditions: 63 | 64 | The above copyright notice and this permission notice shall be included in all 65 | copies or substantial portions of the Software. 66 | 67 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 68 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 69 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 70 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 71 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 72 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 73 | SOFTWARE. 74 | 75 | License for "Bloodhound.py": 76 | 77 | MIT License 78 | 79 | Copyright (c) 2018 Fox-IT 80 | 81 | Permission is hereby granted, free of charge, to any person obtaining a copy 82 | of this software and associated documentation files (the "Software"), to deal 83 | in the Software without restriction, including without limitation the rights 84 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 85 | copies of the Software, and to permit persons to whom the Software is 86 | furnished to do so, subject to the following conditions: 87 | 88 | The above copyright notice and this permission notice shall be included in all 89 | copies or substantial portions of the Software. 90 | 91 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 92 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 93 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 94 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 95 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 96 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 97 | SOFTWARE. 98 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | rm -f -r build/ 3 | rm -f -r dist/ 4 | rm -f -r *.egg-info 5 | find . -name '*.pyc' -exec rm -f {} + 6 | find . -name '*.pyo' -exec rm -f {} + 7 | find . -name '*~' -exec rm -f {} + 8 | 9 | publish: clean 10 | python3 setup.py sdist bdist_wheel 11 | python3 -m twine upload dist/* 12 | 13 | rebuild: clean 14 | python3 setup.py install 15 | 16 | build: 17 | python3 setup.py install -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Supported Python versions](https://img.shields.io/badge/python-3.6+-blue.svg) [![Documentation Status](https://readthedocs.org/projects/msldap/badge/?version=latest)](https://msldap.readthedocs.io/en/latest/?badge=latest) [![Twitter](https://img.shields.io/twitter/follow/skelsec?label=skelsec&style=social)](https://twitter.com/intent/follow?screen_name=skelsec) 2 | 3 | ## :triangular_flag_on_post: Sponsors 4 | 5 | If you like this project, consider purchasing licenses of [OctoPwn](https://octopwn.com/), our full pentesting suite that runs in your browser! 6 | For notifications on new builds/releases and other info, hop on to our [Discord](https://discord.gg/PM8utcNxMS) 7 | 8 | # msldap 9 | LDAP library for MS AD 10 | ![Documentation Status](https://user-images.githubusercontent.com/19204702/81515211-3761e880-9333-11ea-837f-bcbe2a67ee48.gif ) 11 | 12 | ## :triangular_flag_on_post: Runs in the browser 13 | 14 | This project, alongside with many other pentester tools runs in the browser with the power of OctoPwn! 15 | Check out the community version at [OctoPwn - Live](https://live.octopwn.com/) 16 | 17 | # Documentation 18 | [Awesome documentation here!](https://msldap.readthedocs.io/en/latest/) 19 | 20 | # Features 21 | - Comes with a built-in console LDAP client 22 | - All parameters can be conrolled via a conveinent URL (see below) 23 | - Supports integrated windows authentication (SSPI) both with NTLM and with KERBEROS 24 | - Supports channel binding (for ntlm and kerberos not SSPI) 25 | - Supports encryption (for NTLM/KERBEROS/SSPI) 26 | - Supports LDAPS (TODO: actually verify certificate) 27 | - Supports SOCKS5 proxy withot the need of extra proxifyer 28 | - Minimal footprint 29 | - A lot of pre-built queries for convenient information polling 30 | - Easy to integrate to your project 31 | - No testing suite 32 | 33 | # Installation 34 | Via GIT 35 | `python3 setup.py install` 36 | OR 37 | `pip install msldap` 38 | 39 | # Prerequisites 40 | - `asn1crypto` module. Some LDAP queries incorporate ASN1 strucutres to be sent on top of the ASN1 transport XD 41 | - `asysocks` module. To support socks proxying. 42 | - `aiocmd` For the interactive client 43 | - `asciitree` For plotting nice trees in the interactive client 44 | 45 | # Usage 46 | Please note that this is a library, and was not intended to be used as a command line program. 47 | Whit this noted, the projects packs a fully functional LDAP interactive client. When installing the `msldap` module with `setup.py install` a new binary will appear called `msldap` (shocking naming conventions) 48 | 49 | # LDAP connection URL 50 | The major change was needed in version 0.2.0 to unify different connection options as one single string, without the need for additional command line switches. 51 | The new connection string is composed in the following manner: 52 | `+://\:@:/?=&=&...` 53 | Detailed explanation with examples: 54 | ``` 55 | +://:@://?= 56 | 57 | 58 | sets the ldap protocol following values supported: 59 | - ldap 60 | - ldaps 61 | - gc 62 | - gc_ssl 63 | 64 | can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: 65 | - ntlm-password 66 | - ntlm-nt 67 | - kerberos-password (dc option param must be used) 68 | - kerberos-rc4 / kerberos-nt (dc option param must be used) 69 | - kerberos-aes (dc option param must be used) 70 | - kerberos-keytab (dc option param must be used) 71 | - kerberos-ccache (dc option param must be used) 72 | - kerberos-pfx (dc option param must be used) 73 | - kerberos-pem (dc option param must be used) 74 | - kerberos-certstore (dc option param must be used, windows only) 75 | - sspi-ntlm (windows only!) 76 | - sspi-kerberos (windows only!) 77 | - anonymous 78 | - plain 79 | - simple 80 | - sicily (same format as ntlm-nt but using the SICILY authentication) 81 | 82 | : 83 | OPTIONAL. Specifies the root tree of all queries 84 | 85 | can be: 86 | - timeout : connction timeout in seconds 87 | - proxytype: currently only socks5 proxy is supported 88 | - proxyhost: Ip or hostname of the proxy server 89 | - proxyport: port of the proxy server 90 | - proxytimeout: timeout ins ecodns for the proxy connection 91 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication 92 | 93 | Examples: 94 | ldap://10.10.10.2 (anonymous bind) 95 | ldaps://test.corp (anonymous bind) 96 | ldap+sspi-ntlm://test.corp 97 | ldap+sspi-kerberos://test.corp 98 | ldap://TEST\\victim:@10.10.10.2 (defaults to SASL GSSAPI NTLM) 99 | ldap+simple://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) 100 | ldap+plain://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) 101 | ldap+ntlm-password://TEST\\victim:@10.10.10.2 102 | ldap+ntlm-nt://TEST\\victim:@10.10.10.2 103 | ldap+kerberos-password://TEST\\victim:@/?dc=10.10.10.2 104 | ldap+kerberos-rc4://TEST\\victim:@/?dc=10.10.10.2 105 | ldap+kerberos-aes://TEST\\victim:@/?dc=10.10.10.2 106 | ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/ 107 | ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44 108 | ``` 109 | 110 | # Kudos 111 | Certificate services functionality was based on [certi](https://github.com/zer1t0/certi) created by @zer1t0 112 | AC-RN 113 | -------------------------------------------------------------------------------- /builder/pyinstaller/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set projectname=msldap 3 | set hiddenimports= --hidden-import cryptography --hidden-import cffi --hidden-import cryptography.hazmat.backends.openssl --hidden-import cryptography.hazmat.bindings._openssl --hidden-import unicrypto --hidden-import unicrypto.backends.pycryptodome.DES --hidden-import unicrypto.backends.pycryptodome.TDES --hidden-import unicrypto.backends.pycryptodome.AES --hidden-import unicrypto.backends.pycryptodome.RC4 --hidden-import unicrypto.backends.pure.DES --hidden-import unicrypto.backends.pure.TDES --hidden-import unicrypto.backends.pure.AES --hidden-import unicrypto.backends.pure.RC4 --hidden-import unicrypto.backends.cryptography.DES --hidden-import unicrypto.backends.cryptography.TDES --hidden-import unicrypto.backends.cryptography.AES --hidden-import unicrypto.backends.cryptography.RC4 --hidden-import unicrypto.backends.pycryptodomex.DES --hidden-import unicrypto.backends.pycryptodomex.TDES --hidden-import unicrypto.backends.pycryptodomex.AES --hidden-import unicrypto.backends.pycryptodomex.RC4 4 | set root=%~dp0 5 | set repo=%root%..\..\%projectname% 6 | IF NOT DEFINED __BUILDALL_VENV__ (GOTO :CREATEVENV) 7 | GOTO :BUILD 8 | 9 | :CREATEVENV 10 | python -m venv %root%\env 11 | CALL %root%\env\Scripts\activate.bat 12 | pip install pyinstaller 13 | GOTO :BUILD 14 | 15 | :BUILD 16 | cd %repo%\..\ 17 | pip install . 18 | cd %repo%\examples 19 | pyinstaller -F msldapclient.py -n msldap %hiddenimports% 20 | pyinstaller -F msldapbloodhound.py -n msldap-bloodhound %hiddenimports% 21 | cd %repo%\examples\dist & copy *.exe %root% 22 | GOTO :CLEANUP 23 | 24 | :CLEANUP 25 | IF NOT DEFINED __BUILDALL_VENV__ (deactivate) 26 | cd %root% 27 | EXIT /B 28 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= python3.7 -msphinx 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-autodoc-typehints 3 | sphinxcontrib-spelling 4 | sphinxcontrib-trio -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | ========================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | client 8 | connection 9 | credential 10 | target 11 | proxy 12 | url -------------------------------------------------------------------------------- /docs/source/client.rst: -------------------------------------------------------------------------------- 1 | MSLDAPClient -- high level LDAP functions 2 | ================================================== 3 | 4 | 5 | .. autoclass:: msldap.client.MSLDAPClient 6 | :members: add, modify, delete, change_password, add_user_to_group, delete_user, add_user_spn, add_additional_hostname, disable_user, enable_user,unlock_user, create_user_dn, create_user, get_all_trusts, get_all_objectacl, get_all_tokengroups, get_tokengroups, get_dn_for_objectsid, get_group_members, get_user_by_dn, get_group_by_dn, get_all_ous, get_all_groups, set_objectacl_by_dn, get_objectacl_by_dn, get_all_knoreq_users, get_all_service_users, get_all_spn_entries, get_ad_info, get_user, get_laps, get_all_laps, get_all_gpos, get_all_machines, get_all_users, get_tree_plot, pagedsearch 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | sys.path.insert(0, os.path.abspath('../..')) 16 | sys.setrecursionlimit(1500) 17 | 18 | from msldap._version import __version__ 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'msldap' 23 | copyright = '2020, Tamas Jos' 24 | author = 'Tamas Jos' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = __version__ 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc'] 35 | 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = [] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'alabaster' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | 58 | master_doc = 'index' -------------------------------------------------------------------------------- /docs/source/connection.rst: -------------------------------------------------------------------------------- 1 | MSLDAPClientConnection -- low level LDAP connection 2 | ====================================================== 3 | 4 | | The connection class provides low-level methods to interface with the server. 5 | | Using this class is not recommended for normal operation. 6 | 7 | .. autoclass:: msldap.connection.MSLDAPClientConnection 8 | :members: connect, disconnect, add, modify, delete, search, pagedsearch 9 | 10 | -------------------------------------------------------------------------------- /docs/source/credential.rst: -------------------------------------------------------------------------------- 1 | MSLDAPCredential -- credential object 2 | ================================================== 3 | 4 | 5 | .. autoclass:: msldap.commons.credential.MSLDAPCredential 6 | 7 | -------------------------------------------------------------------------------- /docs/source/factory.rst: -------------------------------------------------------------------------------- 1 | LDAPConnectionFactory 2 | ================================================== 3 | 4 | 5 | .. autoclass:: msldap.commons.url.LDAPConnectionFactory 6 | :members: get_credential, get_target, get_client, get_connection 7 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. msldap documentation master file, created by 2 | sphinx-quickstart on Sat May 9 03:33:11 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | , get_dn_for_objectsid, get_group_members 6 | 7 | Welcome to msldap's documentation! 8 | ================================== 9 | 10 | Basic example 11 | ---------------------------- 12 | | All basic examples will fetch the user object of the user `Administrator` and print out the attributes 13 | | The difference is the type of authentication and security settings which are controlled by the `url` parameter 14 | | 15 | | This module supports a wide variety of authentication and channel protection mechanisms, check the Tutorials! 16 | 17 | .. literalinclude:: ../../examples/client.py 18 | :emphasize-lines: 4 19 | 20 | 21 | Tutorials 22 | --------- 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | 27 | tutorial 28 | 29 | API Reference 30 | -------------- 31 | 32 | .. toctree:: 33 | :maxdepth: 2 34 | 35 | api -------------------------------------------------------------------------------- /docs/source/proxy.rst: -------------------------------------------------------------------------------- 1 | MSLDAPProxy -- proxy object 2 | ================================================== 3 | 4 | 5 | .. autoclass:: msldap.commons.proxy.MSLDAPProxy 6 | :members: from_params 7 | -------------------------------------------------------------------------------- /docs/source/target.rst: -------------------------------------------------------------------------------- 1 | MSLDAPTarget -- connection target object 2 | ================================================== 3 | 4 | 5 | .. autoclass:: msldap.commons.target.MSLDAPTarget 6 | 7 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ========================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | urltutorial -------------------------------------------------------------------------------- /docs/source/urltutorial.rst: -------------------------------------------------------------------------------- 1 | URL based examples 2 | ################### 3 | | Both the connection and the authentication can be controlled via the `url` parameter. 4 | | The same example code will be used in this tutorial, only the `url` parameter will change 5 | 6 | Sample code 7 | """""""""""" 8 | .. literalinclude:: ../../examples/client.py 9 | :emphasize-lines: 4 10 | 11 | Authentication 12 | """"""""""""""" 13 | 14 | Simple bind 15 | --------------- 16 | | username and password 17 | 18 | .. code:: python 19 | 20 | 'ldap+simple://TEST\\victim:Passw0rd!1@10.10.10.2' 21 | 22 | Sicily bind 23 | --------------- 24 | | The sicily bind was created by Microsoft and provides the same mechanisms as "GSSAPI - NTLM" 25 | 26 | | username and password 27 | 28 | .. code:: python 29 | 30 | 'ldap+sicily://TEST\\victim:Passw0rd!1@10.10.10.2' 31 | 32 | 33 | GSSAPI - NTLM bind 34 | -------------------- 35 | | username and password 36 | 37 | .. code:: python 38 | 39 | 'ldap+ntlm-password://TEST\\victim:Passw0rd!1@10.10.10.2' 40 | 41 | | NT hash of the user 42 | 43 | .. code:: python 44 | 45 | 'ldap+ntlm-nt://TEST\\victim:f8963568a1ec62a3161d9d6449baba93@10.10.10.2' 46 | 47 | | SSPI integrated auth. This will use the current user's authentication context. The username doesn't matter, but the correct domain must be set! Windows only 48 | 49 | .. code:: python 50 | 51 | 'ldap+sspi-ntlm://TEST\\victim@10.10.10.2' 52 | 53 | GSSAPI - Kerberos bind 54 | ------------------------ 55 | .. warning:: For kerberos authentication type, the `dc` parameter with the kerberos server's IP address must be set! 56 | 57 | | username and password 58 | | this allows they kerberos ticket encryption type to be set with the `etype` parameter 59 | 60 | .. code:: python 61 | 62 | 'ldap+kerberos-password://TEST\\victim:Passw0rd!1@10.10.10.2/?dc=10.10.10.2' 63 | .. code:: python 64 | 65 | 'ldap+kerberos-password://TEST\\victim:Passw0rd!1@10.10.10.2/?dc=10.10.10.2&etype=23' 66 | 67 | | RC4 key (same as NT hash) 68 | 69 | .. code:: python 70 | 71 | 'ldap+kerberos-rc4://TEST\\victim:f8963568a1ec62a3161d9d6449baba93@10.10.10.2/?dc=10.10.10.2' 72 | 73 | | AES key (both 128 and 256 bits supported) 74 | 75 | .. code:: python 76 | 77 | 'ldap+kerberos-aes://TEST\\victim:XXXXX@10.10.10.2/?dc=10.10.10.2' 78 | 79 | | SSPI integrated auth. 80 | | This will use the current user's authentication context. 81 | | The username doesn't matter, but the correct domain must be set! Windows only 82 | 83 | .. code:: python 84 | 85 | 'ldap+sspi-kerberos://TEST\\victim@10.10.10.2/?dc=10.10.10.2' 86 | 87 | 88 | Anonymous Bind 89 | -------------------- 90 | | Currently only the `simple bind` provides anonymous auth 91 | 92 | .. code:: python 93 | 94 | 'ldap+simple://10.10.10.2' 95 | 96 | 97 | Connection 98 | """"""""""""""" 99 | Various connection options available. Most of them are listed below. 100 | 101 | LDAPS 102 | -------------------- 103 | | LDAP-over-SSL can be selected by replacing the `ldap` specification in the `url` parameter with `ldaps` 104 | 105 | .. warning:: For a successful connection over LDAPS the proper hostname of the server must be used! 106 | 107 | .. code:: python 108 | 109 | 'ldaps+simple://dc1.test.corp' 110 | 111 | Channel Binding 112 | -------------------- 113 | | When LDAPS is used, the module automatically performs channel binding. No additional changes necessary 114 | 115 | Encryption 116 | -------------------- 117 | | When GSSAPI authentication is used, the encryption can be turned on to provide more security. 118 | | This is done by the `encrypt` parameter added to the `url`. 119 | | It is not enabled by default, as it can slow down the connection considerably. 120 | 121 | .. warning:: Channel encryption MUST NOT be used together with LDAPS! Doing so will result in failed connection! (this limitation is in the server implementation, not in msldap) 122 | 123 | .. code:: python 124 | 125 | 'ldap+ntlm-password://TEST\\victim:Passw0rd!1@10.10.10.2/?encrypt=1' 126 | 127 | Proxy 128 | -------------------- 129 | | Socks4 and Socks5 proxying is fully supported. 130 | | Proxy settings are controlled via additional url parameters 131 | | The following attributes must be set: 132 | | proxyhost - IP address or hostname of the proxy server 133 | | proxyport - port of the proxy service 134 | | proxytype - type os the proxy. Can be `socks5` or `socks4` 135 | 136 | .. code:: python 137 | 138 | 'ldap+ntlm-password://TEST\\victim:Passw0rd!1@10.10.10.2/?proxyhost=127.0.0.1&proxyport=1080&proxytype=socks5' -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from msldap.commons.factory import LDAPConnectionFactory 3 | 4 | url = 'ldap+simple://TEST\\victim:Passw0rd!1@10.10.10.2' 5 | 6 | async def client(url): 7 | conn_url = LDAPConnectionFactory.from_url(url) 8 | ldap_client = conn_url.get_client() 9 | _, err = await ldap_client.connect() 10 | if err is not None: 11 | raise err 12 | 13 | user = await ldap_client.get_user('Administrator') 14 | print(str(user)) 15 | 16 | if __name__ == '__main__': 17 | asyncio.run(client(url)) 18 | -------------------------------------------------------------------------------- /msldap/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import logging 8 | 9 | logger = logging.getLogger(__name__) 10 | handler = logging.StreamHandler() 11 | formatter = logging.Formatter( 12 | '%(asctime)s %(name)-12s %(levelname)-8s %(message)s') 13 | handler.setFormatter(formatter) 14 | logger.addHandler(handler) 15 | logger.setLevel(logging.INFO) 16 | -------------------------------------------------------------------------------- /msldap/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import logging 8 | import csv 9 | from msldap.connection import MSLDAPClientConnection 10 | from msldap.commons.factory import LDAPConnectionFactory 11 | from msldap.ldap_objects import MSADUser, MSADMachine 12 | 13 | 14 | def run(): 15 | import argparse 16 | parser = argparse.ArgumentParser(description='MS LDAP library') 17 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') 18 | parser.add_argument('connection', help='Connection string in URL format.') 19 | parser.add_argument('--tree', help='LDAP tree to perform the searches on') 20 | 21 | subparsers = parser.add_subparsers(help = 'commands') 22 | subparsers.required = True 23 | subparsers.dest = 'command' 24 | 25 | dump_group = subparsers.add_parser('dump', help='Dump all user objects to TSV file') 26 | dump_group.add_argument('outfile', help='output file') 27 | 28 | spn_group = subparsers.add_parser('spn', help='Dump all users with servicePrincipalName attribute set to TSV file') 29 | spn_group.add_argument('outfile', help='output file') 30 | 31 | dsa_group = subparsers.add_parser('dsa', help='Grab basic info about the AD') 32 | 33 | args = parser.parse_args() 34 | 35 | 36 | ###### VERBOSITY 37 | if args.verbose == 0: 38 | logging.basicConfig(level=logging.INFO) 39 | else: 40 | logging.basicConfig(level=logging.DEBUG) 41 | 42 | url_dec = LDAPConnectionFactory.from_url(args.connection) 43 | creds = url_dec.get_credential() 44 | target = url_dec.get_target() 45 | print(str(creds)) 46 | print(str(target)) 47 | connection = MSLDAPClientConnection(creds, target) 48 | 49 | if args.command == 'dsa': 50 | print(connection.get_server_info()) 51 | 52 | elif args.command == 'dump': 53 | connection.connect() 54 | adinfo = connection.get_ad_info() 55 | with open(args.outfile, 'w', newline='', encoding = 'utf8') as f: 56 | writer = csv.writer(f, delimiter = '\t') 57 | writer.writerow(MSADUser.TSV_ATTRS) 58 | for user in connection.get_all_user_objects(): 59 | writer.writerow(user.get_row(MSADUser.TSV_ATTRS)) 60 | 61 | with open(args.outfile + '_comp', 'w', newline='', encoding = 'utf8') as f: 62 | writer = csv.writer(f, delimiter = '\t') 63 | writer.writerow(MSADMachine.TSV_ATTRS) 64 | for comp in connection.get_all_machine_objects(): 65 | writer.writerow(comp.get_row(MSADMachine.TSV_ATTRS)) 66 | 67 | elif args.command == 'spn': 68 | connection.connect() 69 | adinfo, err = connection.get_ad_info() 70 | with open(args.outfile, 'w', newline='', encoding = 'utf8') as f: 71 | for user in connection.get_all_service_user_objects(): 72 | f.write(user.sAMAccountName + '\r\n') 73 | 74 | if __name__ == '__main__': 75 | run() 76 | -------------------------------------------------------------------------------- /msldap/_version.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.5.15" 3 | __banner__ = \ 4 | """ 5 | # msldap %s 6 | # Author: Tamas Jos @skelsec (info@skelsecprojects.com) 7 | """ % __version__ -------------------------------------------------------------------------------- /msldap/commons/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/commons/__init__.py -------------------------------------------------------------------------------- /msldap/commons/authbuilder.py: -------------------------------------------------------------------------------- 1 | from asyauth.common.credentials.spnego import SPNEGOCredential 2 | from asyauth.common.constants import asyauthProtocol 3 | from asyauth.common.credentials import UniCredential 4 | 5 | def get_auth_context(credential:UniCredential): 6 | if credential.protocol in [asyauthProtocol.NTLM, asyauthProtocol.KERBEROS]: 7 | spnego = SPNEGOCredential([credential]) 8 | return spnego.build_context() 9 | 10 | elif credential.protocol == asyauthProtocol.SICILY: 11 | return credential.build_context() 12 | 13 | elif credential.protocol in [asyauthProtocol.SIMPLE, asyauthProtocol.PLAIN, asyauthProtocol.NONE, asyauthProtocol.SSL]: 14 | return credential 15 | 16 | else: 17 | raise Exception('Unsupported authentication protocol "%s"' % credential.protocol) 18 | -------------------------------------------------------------------------------- /msldap/commons/common.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | class MSLDAPClientStatus(enum.Enum): 4 | CONNECTED = 'CONNECTED' 5 | RUNNING = 'RUNNING' 6 | STOPPED = 'STOPPED' 7 | ERROR = 'ERROR' 8 | -------------------------------------------------------------------------------- /msldap/commons/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | from msldap.protocol.messages import resultCode 3 | 4 | 5 | LDAPResultCodeLookup ={ 6 | 0 : 'success', 7 | 1 : 'operationsError', 8 | 2 : 'protocolError', 9 | 3 : 'timeLimitExceeded', 10 | 4 : 'sizeLimitExceeded', 11 | 5 : 'compareFalse', 12 | 6 : 'compareTrue', 13 | 7 : 'authMethodNotSupported', 14 | 8 : 'strongerAuthRequired', 15 | 10 : 'referral', 16 | 11 : 'adminLimitExceeded', 17 | 12 : 'unavailableCriticalExtension', 18 | 13 : 'confidentialityRequired', 19 | 14 : 'saslBindInProgress', 20 | 16 : 'noSuchAttribute', 21 | 17 : 'undefinedAttributeType', 22 | 18 : 'inappropriateMatching', 23 | 19 : 'constraintViolation', 24 | 20 : 'attributeOrValueExists', 25 | 21 : 'invalidAttributeSyntax', 26 | 32 : 'noSuchObject', 27 | 33 : 'aliasProblem', 28 | 34 : 'invalidDNSyntax', 29 | 36 : 'aliasDereferencingProblem', 30 | 48 : 'inappropriateAuthentication', 31 | 49 : 'invalidCredentials', 32 | 50 : 'insufficientAccessRights', 33 | 51 : 'busy', 34 | 52 : 'unavailable', 35 | 53 : 'unwillingToPerform', 36 | 54 : 'loopDetect', 37 | 64 : 'namingViolation', 38 | 65 : 'objectClassViolation', 39 | 66 : 'notAllowedOnNonLeaf', 40 | 67 : 'notAllowedOnRDN', 41 | 68 : 'entryAlreadyExists', 42 | 69 : 'objectClassModsProhibited', 43 | 71 : 'affectsMultipleDSAs', 44 | 80 : 'other', 45 | } 46 | LDAPResultCodeLookup_inv = {v: k for k, v in LDAPResultCodeLookup.items()} 47 | 48 | class LDAPServerException(Exception): 49 | def __init__(self, resultname, diagnostic_message, message = None): 50 | self.resultcode = LDAPResultCodeLookup_inv[resultname] 51 | self.resultname = resultname 52 | self.diagnostic_message = diagnostic_message 53 | self.message = message 54 | if self.message is None: 55 | self.message = 'LDAP server sent error! Result code: "%s" Reason: "%s"' % (self.resultcode, self.diagnostic_message) 56 | super().__init__(self.message) 57 | 58 | class LDAPSearchException(LDAPServerException): 59 | def __init__(self, resultcode, diagnostic_message): 60 | message = 'LDAP Search failed! Result code: "%s" Reason: "%s"' % (resultcode, diagnostic_message) 61 | super().__init__(resultcode, diagnostic_message, message) 62 | 63 | class LDAPBindException(LDAPServerException): 64 | def __init__(self, resultcode, diagnostic_message): 65 | message = 'LDAP Bind failed! Result code: "%s" Reason: "%s"' % (resultcode, diagnostic_message) 66 | super().__init__(resultcode, diagnostic_message, message) 67 | 68 | class LDAPAddException(LDAPServerException): 69 | def __init__(self, dn, resultcode, diagnostic_message): 70 | self.dn = dn 71 | message = 'LDAP Add operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) 72 | super().__init__(resultcode, diagnostic_message, message) 73 | 74 | class LDAPModifyException(LDAPServerException): 75 | def __init__(self, dn, resultcode, diagnostic_message): 76 | self.dn = dn 77 | message = 'LDAP Modify operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) 78 | super().__init__(resultcode, diagnostic_message, message) 79 | 80 | class LDAPDeleteException(LDAPServerException): 81 | def __init__(self, dn, resultcode, diagnostic_message): 82 | self.dn = dn 83 | message = 'LDAP Delete operation failed on DN %s! Result code: "%s" Reason: "%s"' % (self.dn, resultcode, diagnostic_message) 84 | super().__init__(resultcode, diagnostic_message, message) 85 | -------------------------------------------------------------------------------- /msldap/commons/factory.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | # 4 | # Author: 5 | # Tamas Jos (@skelsec) 6 | # 7 | import enum 8 | import copy 9 | 10 | from msldap.commons.target import MSLDAPTarget 11 | from msldap.client import MSLDAPClient 12 | from msldap.connection import MSLDAPClientConnection 13 | from asyauth.common.credentials import UniCredential 14 | 15 | class LDAPConnectionFactory: 16 | """ 17 | The URL describes both the connection target and the credentials. This class creates all necessary objects to set up the client. 18 | 19 | :param url: 20 | :type url: str 21 | """ 22 | 23 | help_epilog = """ 24 | MSLDAP URL Format: +://:@://?= 25 | sets the ldap protocol following values supported: 26 | - ldap 27 | - ldaps 28 | can be omitted if plaintext authentication is to be performed (in that case it default to ntlm-password), otherwise: 29 | - ntlm-password 30 | - ntlm-nt 31 | - kerberos-password (dc option param must be used) 32 | - kerberos-rc4 / kerberos-nt (dc option param must be used) 33 | - kerberos-aes (dc option param must be used) 34 | - kerberos-keytab (dc option param must be used) 35 | - kerberos-ccache (dc option param must be used) 36 | - sspi-ntlm (windows only!) 37 | - sspi-kerberos (windows only!) 38 | - anonymous 39 | - plain 40 | - simple 41 | - sicily (same format as ntlm-nt but using the SICILY authentication) 42 | : 43 | OPTIONAL. Specifies the root tree of all queries 44 | can be: 45 | - timeout : connction timeout in seconds 46 | - proxytype: currently only socks5 proxy is supported 47 | - proxyhost: Ip or hostname of the proxy server 48 | - proxyport: port of the proxy server 49 | - proxytimeout: timeout in secodns for the proxy connection 50 | - dc: the IP address of the domain controller, MUST be used for kerberos authentication 51 | - encrypt: enable encryption. Only for NTLM. DOESNT WORK WITH LDAPS 52 | - etype: Supported encryption types for Kerberos authentication. Multiple can be specified. 53 | - rate: LDAP paged search query rate limit. Will sleep for seconds between each new page. Default: 0 (no limit) 54 | - pagesize: LDAP paged search query size per page. Max: 1000. Default: 1000 55 | 56 | Examples: 57 | ldap://10.10.10.2 (anonymous bind) 58 | ldaps://test.corp (anonymous bind) 59 | ldap+sspi-ntlm://test.corp 60 | ldap+sspi-kerberos://test.corp 61 | ldap://TEST\\victim:@10.10.10.2 (defaults to SASL GSSAPI NTLM) 62 | ldap+simple://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) 63 | ldap+plain://TEST\\victim:@10.10.10.2 (SASL SIMPLE auth) 64 | ldap+ntlm-password://TEST\\victim:@10.10.10.2 65 | ldap+ntlm-nt://TEST\\victim:@10.10.10.2 66 | ldap+kerberos-password://TEST\\victim:@10.10.10.2 67 | ldap+kerberos-rc4://TEST\\victim:@10.10.10.2 68 | ldap+kerberos-aes://TEST\\victim:@10.10.10.2 69 | ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/ 70 | ldap://TEST\\victim:password@10.10.10.2/DC=test,DC=corp/?timeout=99&proxytype=socks5&proxyhost=127.0.0.1&proxyport=1080&proxytimeout=44 71 | """ 72 | 73 | def __init__(self, credential:UniCredential = None, target:MSLDAPTarget = None ): 74 | self.credential = credential 75 | self.target = target 76 | 77 | @staticmethod 78 | def from_url(connection_url): 79 | target = MSLDAPTarget.from_url(connection_url) 80 | credential = UniCredential.from_url(connection_url) 81 | return LDAPConnectionFactory(credential, target) 82 | 83 | def get_credential(self) -> UniCredential: 84 | """ 85 | Creates a credential object 86 | 87 | :return: Credential object 88 | :rtype: :class:`UniCredential` 89 | """ 90 | return copy.deepcopy(self.credential) 91 | 92 | def get_target(self) -> MSLDAPTarget: 93 | """ 94 | Creates a target object 95 | 96 | :return: Target object 97 | :rtype: :class:`MSLDAPTarget` 98 | """ 99 | return copy.deepcopy(self.target) 100 | 101 | def get_client(self) -> MSLDAPClient: 102 | """ 103 | Creates a client that can be used to interface with the server 104 | 105 | :return: LDAP client 106 | :rtype: :class:`MSLDAPClient` 107 | """ 108 | cred = self.get_credential() 109 | target = self.get_target() 110 | return MSLDAPClient(target, cred) 111 | 112 | 113 | def get_connection(self) -> MSLDAPClientConnection: 114 | """ 115 | Creates a connection that can be used to interface with the server 116 | 117 | :return: LDAP connection 118 | :rtype: :class:`MSLDAPClientConnection` 119 | """ 120 | cred = self.get_credential() 121 | target = self.get_target() 122 | return MSLDAPClientConnection(target, cred) 123 | 124 | @staticmethod 125 | def from_ldapconnection(connection:MSLDAPClientConnection): 126 | """Creates a new LDAPConnectionFactory object from an existing SMBConnection object""" 127 | """This is useful when you have a connection object, but you need to create a new connection with the same credentials""" 128 | return LDAPConnectionFactory(copy.deepcopy(connection.credential), copy.deepcopy(connection.target)) 129 | 130 | 131 | def __str__(self): 132 | t = '==== LDAPConnectionFactory ====\r\n' 133 | for k in self.__dict__: 134 | val = self.__dict__[k] 135 | if isinstance(val, enum.IntFlag): 136 | val = val 137 | elif isinstance(val, enum.Enum): 138 | val = val.name 139 | 140 | t += '%s: %s\r\n' % (k, str(val)) 141 | 142 | return t 143 | 144 | if __name__ == '__main__': 145 | url_tests = [ 146 | 'ldap://10.10.10.2', 147 | 'ldap://10.10.10.2:9999', 148 | 'ldap://test:password@10.10.10.2', 149 | 'ldap://domain\\test@10.10.10.2', 150 | 'ldap://domain\\test:password@10.10.10.2:9999', 151 | 'ldap://domain\\test:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@10.10.10.2:9999', 152 | 'ldaps+sspi-ntlm://10.10.10.2', 153 | 'ldaps+sspi-kerberos://10.10.10.2', 154 | 'ldaps+ntlm-password://domain\\test:password@10.10.10.2:9999', 155 | 'ldaps+ntlm-nt://domain\\test:password@10.10.10.2:9999', 156 | 'ldaps+kerberos-password://domain\\test:password@10.10.10.2:9999', 157 | 'ldaps://10.10.10.2:9999', 158 | 'ldaps://test:password@10.10.10.2', 159 | 'ldaps://domain\\test@10.10.10.2', 160 | 'ldaps://domain\\test:password@10.10.10.2:9999', 161 | 'ldaps://DOMAIN\\test:password@10.10.10.2:9999/?proxytype=socks5&proxyserver=127.0.0.1', 162 | 'ldaps://DOMAIN\\test:password@10.10.10.2:9999/?proxytype=socks5&proxyserver=127.0.0.1&proxyuser=admin&proxypass=alma', 163 | 'ldaps://DOMAIN\\test:password@10.10.10.2:9999/?proxytype=multiplexor&proxyserver=127.0.0.1&proxyport=9999&proxyuser=admin&proxypass=alma', 164 | 'ldaps://10.10.10.2', 165 | 'ldaps://10.10.10.2:6666', 166 | ] 167 | for url in url_tests: 168 | print('===========================================================================') 169 | print(url) 170 | try: 171 | dec = LDAPConnectionFactory.from_url(url) 172 | creds = dec.get_credential() 173 | target = dec.get_target() 174 | except Exception as e: 175 | import traceback 176 | traceback.print_exc() 177 | print('ERROR! Reason: %s' % e) 178 | input() 179 | else: 180 | print(str(creds)) 181 | print(str(target)) 182 | input() 183 | -------------------------------------------------------------------------------- /msldap/commons/ldif.py: -------------------------------------------------------------------------------- 1 | import io 2 | import base64 3 | from typing import Dict, List 4 | 5 | class LDIFIdx: 6 | def __init__(self, start, end): 7 | self.start = start 8 | self.end = end 9 | self.length = end - start 10 | 11 | class MSLDAPLdiff: 12 | def __init__(self, max_cache_size:int = 10000): 13 | self.filename:str = None 14 | self.filehandle:io.BytesIO = None 15 | self.dn_index:Dict[str, LDIFIdx] = {} 16 | self.objectclass_index = {} 17 | self.samaccounttype_index = {} 18 | self.objecttype_index = {} 19 | 20 | self.max_cache_size = max_cache_size 21 | self.dncache:Dict[str, List[Dict[str, str]]] = {} 22 | 23 | @staticmethod 24 | async def from_file(filename:str): 25 | ldiff = MSLDAPLdiff() 26 | ldiff.filename = filename 27 | await ldiff.parse() 28 | 29 | async def open_or_handle(self): 30 | if self.filehandle is None: 31 | self.filehandle = open(self.filename, 'r', encoding='utf-8') 32 | return self.filehandle 33 | 34 | async def build_index(self): 35 | print('[+] Building index...') 36 | with open(self.filename, 'r', encoding='utf-8') as f: 37 | while True: 38 | pos = f.tell() # Current position in the file 39 | line = f.readline() 40 | 41 | if not line: # End of file 42 | if current_dn is not None: 43 | # Store the end position for the last entry 44 | self.dn_index[current_dn] = LDIFIdx(start_pos, pos) 45 | break 46 | 47 | if line.startswith('dn: '): 48 | if current_dn is not None: 49 | # Store the end position for the previous entry 50 | self.dn_index[current_dn] = LDIFIdx(start_pos, pos) 51 | 52 | current_dn = line.strip().upper() 53 | start_pos = pos 54 | if line.startswith('objectClass: '): 55 | objectclass = line.strip().upper() 56 | if objectclass not in self.objectclass_index: 57 | self.objectclass_index[objectclass] = [] 58 | self.objectclass_index[objectclass].append(current_dn) 59 | 60 | if line.startswith('sAMAccountType: '): 61 | objectclass = line.strip().upper() 62 | if objectclass not in self.objectclass_index: 63 | self.objectclass_index[objectclass] = [] 64 | self.samaccounttype_index[objectclass].append(current_dn) 65 | 66 | if line.startswith('objectType: '): 67 | objectclass = line.strip().upper() 68 | if objectclass not in self.objectclass_index: 69 | self.objectclass_index[objectclass] = [] 70 | self.objecttype_index[objectclass].append(current_dn) 71 | 72 | elif not line.strip() and current_dn is not None: 73 | # Optional: Handle blank lines between entries if needed 74 | self.dn_index[current_dn] = LDIFIdx(start_pos, pos) 75 | current_dn = None 76 | 77 | 78 | async def fetch(self, dn:str): 79 | dn = dn.upper() 80 | if dn in self.dncache: 81 | return self.dncache[dn] 82 | 83 | if dn not in self.dn_index: 84 | return None 85 | 86 | raw_entry = [] 87 | f = await self.open_or_handle() 88 | idx = self.dn_index[dn] 89 | f.seek(idx.start) 90 | data = f.read(idx.length) 91 | for line in data.split('\n'): 92 | line = line.strip() 93 | if line == '': 94 | continue 95 | if line.startswith('#'): 96 | continue 97 | raw_entry.append(line) 98 | 99 | entry = self.parse_entry(raw_entry) 100 | if len(self.dncache) > self.max_cache_size: 101 | self.dncache.popitem() 102 | 103 | def parse_entry(self, raw_entry:List[str]): 104 | ed = {} 105 | for line in raw_entry: 106 | line = line.strip() 107 | if line == '': 108 | continue 109 | if line.startswith('#'): 110 | continue 111 | 112 | key, value = line.split(':', 1) 113 | key = key.strip() 114 | value = value.strip() 115 | 116 | if line.split(':', 1)[0].endswith('::'): 117 | value = base64.b64decode(value) 118 | 119 | if key not in ed: 120 | ed[key] = [] 121 | 122 | ed[key].append(value) 123 | return ed 124 | 125 | async def parse(self): 126 | await self.build_index() 127 | -------------------------------------------------------------------------------- /msldap/commons/plugin.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | class MSLDAPConsolePlugin(ABC): 4 | def __init__(self, console, connection): 5 | self.console = console 6 | self.connection = connection 7 | 8 | @abstractmethod 9 | async def run(self, runargs:str): 10 | pass 11 | 12 | 13 | 14 | class SamplePlugin(MSLDAPConsolePlugin): 15 | async def run(self, runargs:str): 16 | print("Hello World") 17 | print("Runargs: %s" % runargs) 18 | await self.console.do_ls() -------------------------------------------------------------------------------- /msldap/commons/target.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | # 4 | # Author: 5 | # Tamas Jos (@skelsec) 6 | # 7 | 8 | from os import stat 9 | from sqlite3 import connect 10 | from asysocks.unicomm.common.target import UniTarget, UniProto 11 | from urllib.parse import urlparse, parse_qs 12 | from asysocks.unicomm.utils.paramprocessor import str_one, int_one, bool_one 13 | 14 | msldaptarget_url_params = { 15 | 'pagesize' : int_one, 16 | 'rate' : int_one, 17 | } 18 | 19 | 20 | class MSLDAPTarget(UniTarget): 21 | """ 22 | Describes the connection to the server. 23 | 24 | :param host: IP address or hostname of the server 25 | :type host: str 26 | :param port: port of the LDAP service running on the server 27 | :type port: int 28 | :param proto: Connection protocol to be used 29 | :type proto: :class:`UniProto` 30 | :param tree: The tree to connect to 31 | :type tree: str 32 | :param proxies: specifies what kind of proxy to be used 33 | :type proxies: :class:`List[UniProxyTarget]` 34 | :param timeout: connection timeout in seconds 35 | :type timeout: int 36 | :param ldap_query_page_size: Maximum number of elements to fetch in each paged_query call. 37 | :type ldap_query_page_size: int 38 | :param ldap_query_ratelimit: rate limit of paged queries. This will cause a sleep (in seconds) between fetching of each page of the query 39 | :type ldap_query_ratelimit: float 40 | :param dc_ip: Ip address of the kerberos server (if kerberos is used) 41 | :type dc_ip: str 42 | """ 43 | def __init__(self, ip, port = 389, protocol = UniProto.CLIENT_TCP, tree = None, proxies = None, timeout = 10, ldap_query_page_size = 1000, ldap_query_ratelimit = 0, dns:str=None, dc_ip:str = None, domain:str = None, hostname:str = None, ssl_ctx = None): 44 | UniTarget.__init__(self, ip, port, protocol, timeout, hostname = hostname, ssl_ctx= ssl_ctx, proxies = proxies, domain = domain, dc_ip = dc_ip, dns=dns) 45 | self.tree = tree 46 | self.ldap_query_page_size = ldap_query_page_size 47 | self.ldap_query_ratelimit = ldap_query_ratelimit 48 | 49 | def to_target_string(self): 50 | return 'ldap/%s@%s' % (self.get_hostname_or_ip(), self.domain) #ldap/WIN2019AD.test.corp @ TEST.CORP 51 | 52 | def get_host(self): 53 | if self.protocol == UniProto.CLIENT_SSL_TCP: 54 | proto = 'ldaps' 55 | elif self.protocol == UniProto.CLIENT_TCP: 56 | proto = 'ldap' 57 | return '%s://%s:%s' % (proto, self.get_hostname_or_ip(), self.port) 58 | 59 | def is_ssl(self): 60 | return self.protocol == UniProto.CLIENT_SSL_TCP 61 | 62 | @staticmethod 63 | def from_url(connection_url): 64 | url_e = urlparse(connection_url) 65 | schemes = [] 66 | for item in url_e.scheme.upper().split('+'): 67 | schemes.append(item.replace('-','_')) 68 | if schemes[0] == 'LDAP': 69 | protocol = UniProto.CLIENT_TCP 70 | port = 389 71 | elif schemes[0] == 'LDAPS': 72 | protocol = UniProto.CLIENT_SSL_TCP 73 | port = 636 74 | elif schemes[0] == 'LDAP_SSL': 75 | protocol = UniProto.CLIENT_SSL_TCP 76 | port = 636 77 | elif schemes[0] == 'LDAP_TCP': 78 | protocol = UniProto.CLIENT_TCP 79 | port= 389 80 | elif schemes[0] == 'LDAP_UDP': 81 | raise NotImplementedError() 82 | protocol = UniProto.CLIENT_UDP 83 | port = 389 84 | elif schemes[0] == 'GC': 85 | protocol = UniProto.CLIENT_TCP 86 | port = 3268 87 | elif schemes[0] == 'GC_SSL': 88 | protocol = UniProto.CLIENT_SSL_TCP 89 | port = 3269 90 | else: 91 | raise Exception('Unknown protocol! %s' % schemes[0]) 92 | 93 | if url_e.port: 94 | port = url_e.port 95 | if port is None: 96 | raise Exception('Port must be provided!') 97 | 98 | path = None 99 | if url_e.path not in ['/', '', None]: 100 | path = url_e.path 101 | 102 | unitarget, extraparams = UniTarget.from_url(connection_url, protocol, port, msldaptarget_url_params) 103 | pagesize = extraparams['pagesize'] if extraparams['pagesize'] is not None else 1000 104 | rate = extraparams['rate'] if extraparams['rate'] is not None else 0 105 | 106 | target = MSLDAPTarget( 107 | unitarget.ip, 108 | port = unitarget.port, 109 | protocol = unitarget.protocol, 110 | tree = path, 111 | proxies = unitarget.proxies, 112 | timeout = unitarget.timeout, 113 | ldap_query_page_size = pagesize, 114 | ldap_query_ratelimit = rate, 115 | dns = unitarget.dns, 116 | dc_ip = unitarget.dc_ip, 117 | domain = unitarget.domain, 118 | hostname = unitarget.hostname, 119 | ssl_ctx = unitarget.ssl_ctx, 120 | ) 121 | return target 122 | 123 | 124 | def __str__(self): 125 | t = '==== MSLDAPTarget ====\r\n' 126 | for k in self.__dict__: 127 | t += '%s: %s\r\n' % (k, self.__dict__[k]) 128 | 129 | return t -------------------------------------------------------------------------------- /msldap/commons/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from ipaddress import IPv4Address, IPv6Address 3 | from typing import Union 4 | 5 | 6 | def timestamp2datetime(dt: Union[bytes, int]) -> datetime.datetime: 7 | """ 8 | Converting Windows timestamps to datetime.datetime format 9 | :param dt: Windows timestamp as array of bytes or integer 10 | :type dt: bytearray | int 11 | :return: datetime.datetime 12 | """ 13 | 14 | if isinstance(dt, bytes): 15 | dt = int.from_bytes(dt, "little") 16 | 17 | return datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=dt / 10) 18 | 19 | 20 | def datetime2timestamp(dt) -> int: 21 | delta = dt - datetime.datetime(1601, 1, 1) 22 | ns = int((delta / datetime.timedelta(microseconds=1)) * 10) 23 | return ns.to_bytes(8, "little", signed=False) 24 | 25 | 26 | def bytes2ipv4(data: bytes) -> str: 27 | return str(IPv4Address(data)) 28 | 29 | 30 | def bytes2ipv6(data: bytes) -> str: 31 | return str(IPv6Address(data)) 32 | 33 | 34 | def wrap(s, w) -> str: 35 | return [s[i : i + w] for i in range(0, len(s), w)] 36 | 37 | 38 | def print_cert(cert, offset=0) -> str: 39 | cert = cert["tbs_certificate"] 40 | blanks = " " * offset 41 | msg = [ 42 | "Cert Subject: %s" % cert["subject"]["common_name"], 43 | "Cert Serial: %s" % cert["serial_number"], 44 | "Cert Start: %s" % cert["validity"]["not_before"], 45 | "Cert End: %s" % cert["validity"]["not_after"], 46 | "Cert Issuer: %s" % cert["issuer"]["common_name"], 47 | ] 48 | return "{}{}".format(blanks, "\n{}".format(blanks).join(msg)) 49 | 50 | 51 | def win_timestamp_to_unix(seconds): 52 | """ 53 | Convert Windows timestamp (100 ns since 1 Jan 1601) to 54 | unix timestamp. 55 | """ 56 | seconds = int(seconds) 57 | if seconds == 0: 58 | return 0 59 | return int((seconds - 116444736000000000) / 10000000) 60 | 61 | 62 | def bh_dt_convert(dt: datetime.datetime): 63 | if dt is None or dt == 0 or dt == "0" or dt == "": 64 | return -1 65 | ts = max(0, int(dt.timestamp())) 66 | return ts 67 | 68 | 69 | FUNCTIONAL_LEVELS = { 70 | 0: "2000 Mixed/Native", 71 | 1: "2003 Interim", 72 | 2: "2003", 73 | 3: "2008", 74 | 4: "2008 R2", 75 | 5: "2012", 76 | 6: "2012 R2", 77 | 7: "2016", 78 | } 79 | 80 | KNOWN_SIDS = { 81 | "S-1-0": "Null Authority", 82 | "S-1-0-0": "Nobody", 83 | "S-1-1": "World Authority", 84 | "S-1-1-0": "Everyone", 85 | "S-1-2": "Local Authority", 86 | "S-1-2-0": "Local", 87 | "S-1-3": "Creator Authority", 88 | "S-1-3-0": "Creator Owner", 89 | "S-1-3-1": "Creator Group", 90 | "S-1-3-4": "Owner Rights", 91 | "S-1-4": "Non-unique Authority", 92 | "S-1-5": "NT Authority", 93 | "S-1-5-1": "Dialup", 94 | "S-1-5-2": "Network", 95 | "S-1-5-3": "Batch", 96 | "S-1-5-4": "Interactive", 97 | "S-1-5-5-X-Y": "Logon Session", 98 | "S-1-5-6": "Service", 99 | "S-1-5-7": "Anonymous", 100 | "S-1-5-9": "Enterprise Domain Controllers", 101 | "S-1-5-10": "Principal Self", 102 | "S-1-5-11": "Authenticated Users", 103 | "S-1-5-12": "Restricted Code", 104 | "S-1-5-13": "Terminal Server Users", 105 | "S-1-5-14": "Remote Interactive Logon", 106 | "S-1-5-17": "IUSR", 107 | "S-1-5-18": "Local System", 108 | "S-1-5-19": "NT Authority Local Service", 109 | "S-1-5-20": "NT Authority Network Service", 110 | "S-1-5-32-544": "Administrators", 111 | "S-1-5-32-545": "Users", 112 | "S-1-5-32-546": "Guests", 113 | "S-1-5-32-547": "Power Users", 114 | "S-1-5-32-548": "Account Operators", 115 | "S-1-5-32-549": "Server Operators", 116 | "S-1-5-32-550": "Print Operators", 117 | "S-1-5-32-551": "Backup Operators", 118 | "S-1-5-32-552": "Replicators", 119 | "S-1-5-32-582": "Storage Replica Administrators", 120 | "S-1-5-64-10": "NTLM Authentication", 121 | "S-1-5-64-14": "SChannel Authentication", 122 | "S-1-5-64-21": "Digest Authentication", 123 | "S-1-5-80": "NT Service", 124 | } 125 | -------------------------------------------------------------------------------- /msldap/dontuse/test.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | from ldap3.operation.search import parse_filter, compile_filter 5 | from ldap3.protocol.schemas.ad2012R2 import ad_2012_r2_schema, ad_2012_r2_dsa_info 6 | from ldap3.protocol.rfc4512 import SchemaInfo 7 | from pyasn1.codec.der import decoder, encoder 8 | 9 | from msldap.protocol.query import query_syntax_converter 10 | from msldap.protocol.messages import LDAPMessage, Filter 11 | 12 | # https://ldap3.readthedocs.io/bind.html 13 | if __name__ == '__main__': 14 | 15 | qry = r'(&(servicePrincipalName=*)(!(sAMAccountName=*$)))' 16 | 17 | schema = SchemaInfo.from_json(ad_2012_r2_schema) 18 | auto_escape = True 19 | auto_encode = True 20 | validator = None 21 | check_names = False 22 | 23 | 24 | res = parse_filter(qry, schema, auto_escape, auto_encode, validator, check_names) 25 | print(repr(res)) 26 | res = compile_filter(res.elements[0]) 27 | print(repr(res)) 28 | print(encoder.encode(res).hex()) 29 | 30 | msg = Filter.load(encoder.encode(res)) 31 | print(msg.native) 32 | 33 | sys.exit() 34 | 35 | qry = r'(sAMAccountName=*)' #'(userAccountControl:1.2.840.113556.1.4.803:=4194304)' #'(sAMAccountName=*)' 36 | #qry = r'(sAMAccountType=805306368)' 37 | #a = query_syntax_converter(qry) 38 | #print(a.native) 39 | #input('press bacon!') 40 | schema = SchemaInfo.from_json(ad_2012_r2_schema) 41 | auto_escape = True 42 | auto_encode = True 43 | validator = None 44 | check_names = False 45 | # 46 | # 47 | res = parse_filter(qry, schema, auto_escape, auto_encode, validator, check_names) 48 | print(repr(res)) 49 | res = compile_filter(res.elements[0]) 50 | # 51 | print(repr(res)) 52 | print(encoder.encode(res).hex()) 53 | #res = encoder.encode(res) 54 | #x = Filter.load(res) 55 | #pprint(x.native) 56 | 57 | 58 | flt = query_syntax_converter(qry) 59 | input(flt.native) 60 | 61 | #res = await client.search_test_2() 62 | #pprint.pprint(res) 63 | #search = bytes.fromhex('30840000007702012663840000006e043c434e3d3430392c434e3d446973706c6179537065636966696572732c434e3d436f6e66696775726174696f6e2c44433d746573742c44433d636f72700a01000a010002010002020258010100870b6f626a656374436c61737330840000000d040b6f626a656374436c617373') 64 | #msg = LDAPMessage.load(search) 65 | -------------------------------------------------------------------------------- /msldap/examples/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | -------------------------------------------------------------------------------- /msldap/examples/jsontest.py: -------------------------------------------------------------------------------- 1 | import json 2 | from tqdm import tqdm 3 | 4 | def compare_links(l1, l2): 5 | for entry in l1: 6 | if not isinstance(entry, dict): 7 | print('TYPE_MISSMATCH', f"{entry} not a dict") 8 | else: 9 | for ce in l2: 10 | if ce['GUID'] == entry['GUID'] and ce['IsEnforced'] == entry['IsEnforced']: 11 | break 12 | else: 13 | print('MISSING_LINK', f"{entry['GUID']} not in l2") 14 | 15 | def compare_aces(l1, l2): 16 | for entry in l1: 17 | if not isinstance(entry, dict): 18 | print('TYPE_MISSMATCH', f"{entry} not a dict") 19 | else: 20 | for ce in l2: 21 | if ce['PrincipalSID'] == entry['PrincipalSID'] and ce['PrincipalType'] == entry['PrincipalType'] and ce['RightName'] == entry['RightName'] and ce['IsInherited'] == entry['IsInherited']: 22 | #print('MATCH', f"{entry['PrincipalSID']} in l2") 23 | break 24 | else: 25 | print('MISSING_ACE', f"{entry['PrincipalSID']} not in l2") 26 | 27 | def compare_objlist(ctype, l1, l2): 28 | for entry in l1: 29 | if not isinstance(entry, dict): 30 | print('TYPE_MISSMATCH %s' % ctype, f"{entry} not a dict") 31 | else: 32 | for ce in l2: 33 | if ce['ObjectIdentifier'] == entry['ObjectIdentifier'] and ce['ObjectType'] == entry['ObjectType']: 34 | #print('MATCH', f"{entry['ObjectIdentifier']} in l2") 35 | break 36 | else: 37 | print('MISSING_%s' % ctype, f"{entry['ObjectIdentifier']} not in l2") 38 | 39 | 40 | def compare_dict(label, d1, d2): 41 | #print('Checking %s' % label) 42 | for k in d1: 43 | #print('Checking %s -> %s' % (label, k)) 44 | if k not in d2: 45 | yield ('MISSING_PARAM',k, f"{k} not in dict") 46 | if isinstance(d1[k], (str, int, float, bool)): 47 | if k not in d2: 48 | yield ('MISSING_PARAM', k, f"'{k}' not in dict") 49 | else: 50 | #print('Checking %s -> %s Exists!' % (label, k)) 51 | if d2[k] != d1[k]: 52 | yield ('NOT_EQ', k, f"'{k}' value not equal in dict. D1: {d1[k]} D2: {d2[k]}") 53 | #else: 54 | # print('Checking %s -> %s Matches!' % (label, k)) 55 | if isinstance(d1[k], list): 56 | if k not in d2: 57 | yield ('MISSING_PARAM', k, f"'{k}' not in dict") 58 | else: 59 | #print('Checking %s -> %s Exists!' % (label, k)) 60 | if isinstance(d2[k], list): 61 | if(len(d1[k]) != len(d2[k])): 62 | yield ('NOT_EQ', k, f"'{k}' length not equal in dict. D1: d1[{k}] D2: d2[{k}]") 63 | if k == 'Links': 64 | compare_links(d1[k], d2[k]) 65 | elif k == 'Aces': 66 | compare_aces(d1[k], d2[k]) 67 | elif k in ['ChildObjects', 'Members']: 68 | compare_objlist(k,d1[k], d2[k]) 69 | else: 70 | for i, item in enumerate(d1[k]): 71 | if i >= len(d2[k]): 72 | yield ('MISSING_PARAM', f"'{k}[{i}]'", f"'{k}[{i}]' not in dict") 73 | else: 74 | for k2 in item: 75 | if k2 not in d2[k][i]: 76 | yield ('MISSING_PARAM', f"'{k}[{i}][{k2}]'", f"'{k}[{i}][{k2}]' not in dict") 77 | else: 78 | yield ('TYPE_MISMATCH', k, f"{k} not a list in dict. D1: {d1[k]} D2: {d2[k]}") 79 | if isinstance(d1[k], dict): 80 | if k not in d2: 81 | yield ('MISSING_PARAM', k, f"'{k}' not in dict") 82 | else: 83 | if isinstance(d2[k], dict): 84 | compare_dict('%s -> %s' % (label, k), d1[k], d2[k]) 85 | else: 86 | yield ('TYPE_MISMATCH', k, f"{k} not a dict in dict") 87 | 88 | 89 | filelist = [ 90 | ('/home/webdev/Desktop/comparer/good/good_domains.json', '/home/webdev/Desktop/projects/msldap/domains.json'), 91 | ('/home/webdev/Desktop/comparer/good/good_groups.json', '/home/webdev/Desktop/projects/msldap/groups.json'), 92 | #('/home/webdev/Desktop/comparer/good/good_users.json', '/home/webdev/Desktop/projects/msldap/users.json'), 93 | ('/home/webdev/Desktop/comparer/good/good_computers.json', '/home/webdev/Desktop/projects/msldap/computers.json'), 94 | ('/home/webdev/Desktop/comparer/good/good_containers.json', '/home/webdev/Desktop/projects/msldap/containers.json'), 95 | ('/home/webdev/Desktop/comparer/good/good_gpos.json', '/home/webdev/Desktop/projects/msldap/gpos.json'), 96 | ('/home/webdev/Desktop/comparer/good/good_ous.json', '/home/webdev/Desktop/projects/msldap/ous.json'), 97 | ] 98 | 99 | for filetuple in filelist: 100 | print('Comparing %s to %s' % filetuple) 101 | with open(filetuple[0]) as f: 102 | f.seek(3) 103 | json2 = json.load(f) 104 | 105 | print('Loading test file...') 106 | with open(filetuple[1]) as f: 107 | json1 = json.load(f) 108 | 109 | bypass_diff = { 110 | 'NOT_EQ': { 111 | 'whencreated': 1 112 | } 113 | } 114 | 115 | total = len(json2['data']) 116 | pbar = tqdm(total=total) 117 | for guser in json2['data']: 118 | pbar.update(1) 119 | for user in json1['data']: 120 | if user['ObjectIdentifier'] == guser['ObjectIdentifier']: 121 | #print(user['Aces']) 122 | #print(guser['Aces']) 123 | for diff_type, param_name, desc in compare_dict(guser['ObjectIdentifier'], guser, user): 124 | if diff_type in bypass_diff: 125 | if param_name in bypass_diff[diff_type]: 126 | continue 127 | print('%s -> %s' % (user['ObjectIdentifier'], desc)) 128 | 129 | #input() 130 | 131 | -------------------------------------------------------------------------------- /msldap/examples/msldapbloodhound.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from msldap.bloodhound import MSLDAPDump2Bloodhound 3 | 4 | async def amain(url): 5 | msldap = MSLDAPDump2Bloodhound(url) 6 | await msldap.run() 7 | 8 | def main(): 9 | import argparse 10 | parser = argparse.ArgumentParser(description='Bloodhound collector for MSLDAP') 11 | parser.add_argument('url', help='LDAP connection URL, or ADEXPLORER dat file path in the form adexplorer://') 12 | print(""" 13 | WARNING: This script is still in development. It is not guaranteed to provide the same results as the original Bloodhound collector. 14 | """) 15 | args = parser.parse_args() 16 | asyncio.run(amain(args.url)) 17 | 18 | if __name__ == '__main__': 19 | main() 20 | -------------------------------------------------------------------------------- /msldap/examples/msldapcompdnslist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import asyncio 8 | import traceback 9 | import logging 10 | 11 | from msldap import logger 12 | from asysocks import logger as sockslogger 13 | from msldap.commons.factory import LDAPConnectionFactory 14 | 15 | class MSLDAPCompDomainList: 16 | def __init__(self, ldap_url): 17 | self.conn_url = ldap_url 18 | self.connection = None 19 | self.adinfo = None 20 | self.ldapinfo = None 21 | self.domain_name = None 22 | 23 | async def login(self): 24 | """Performs connection and login""" 25 | try: 26 | logger.debug(self.conn_url.get_credential()) 27 | logger.debug(self.conn_url.get_target()) 28 | 29 | 30 | self.connection = self.conn_url.get_client() 31 | _, err = await self.connection.connect() 32 | if err is not None: 33 | raise err 34 | 35 | return True, None 36 | except Exception as e: 37 | return False, e 38 | 39 | async def do_adinfo(self, show = True): 40 | """Prints detailed Active Driectory info""" 41 | try: 42 | if self.adinfo is None: 43 | self.adinfo = self.connection._ldapinfo 44 | self.domain_name = self.adinfo.distinguishedName.replace('DC','').replace('=','').replace(',','.') 45 | if show is True: 46 | print(self.adinfo) 47 | 48 | return True, None 49 | except Exception as e: 50 | return False, e 51 | 52 | async def run(self): 53 | try: 54 | _, err = await self.login() 55 | if err is not None: 56 | raise err 57 | _, err = await self.do_adinfo(False) 58 | if err is not None: 59 | raise err 60 | 61 | async for machine, err in self.connection.get_all_machines(attrs=['sAMAccountName', 'dNSHostName']): 62 | if err is not None: 63 | raise err 64 | 65 | dns = machine.dNSHostName 66 | if dns is None: 67 | dns = '%s.%s' % (machine.sAMAccountName[:-1], self.domain_name) 68 | 69 | print(str(dns)) 70 | except: 71 | traceback.print_exc() 72 | 73 | 74 | def main(): 75 | import argparse 76 | parser = argparse.ArgumentParser(description='MS LDAP library') 77 | parser.add_argument('-v', '--verbose', action='count', default=0, help='Verbosity, can be stacked') 78 | parser.add_argument('-n', '--no-interactive', action='store_true') 79 | parser.add_argument('url', help='Connection string in URL format.') 80 | 81 | args = parser.parse_args() 82 | 83 | 84 | ###### VERBOSITY 85 | if args.verbose == 0: 86 | logging.basicConfig(level=logging.INFO) 87 | else: 88 | sockslogger.setLevel(logging.DEBUG) 89 | logger.setLevel(logging.DEBUG) 90 | logging.basicConfig(level=logging.DEBUG) 91 | 92 | ldap_url = LDAPConnectionFactory.from_url(args.url) 93 | compdomlist = MSLDAPCompDomainList(ldap_url) 94 | 95 | 96 | asyncio.run(compdomlist.run()) 97 | 98 | if __name__ == '__main__': 99 | main() -------------------------------------------------------------------------------- /msldap/examples/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/examples/utils/__init__.py -------------------------------------------------------------------------------- /msldap/examples/utils/completers.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Callable, Iterable, List, Optional 3 | 4 | from prompt_toolkit.completion import CompleteEvent, Completer, Completion 5 | from prompt_toolkit.document import Document 6 | import traceback 7 | 8 | class PathCompleter(Completer): 9 | """ 10 | Complete for Path variables. 11 | 12 | :param get_paths: Callable which returns a list of directories to look into 13 | when the user enters a relative path. 14 | :param file_filter: Callable which takes a filename and returns whether 15 | this file should show up in the completion. ``None`` 16 | when no filtering has to be done. 17 | :param min_input_len: Don't do autocompletion when the input string is shorter. 18 | """ 19 | def __init__(self, only_directories: bool = False, 20 | get_current_dirs: Optional[Callable[[], List[str]]] = None, 21 | file_filter: Optional[Callable[[str], bool]] = None, 22 | min_input_len: int = 0, 23 | expanduser: bool = False) -> None: 24 | 25 | self.only_directories = only_directories 26 | self.get_current_dirs = get_current_dirs or (lambda: ['.']) 27 | self.file_filter = file_filter or (lambda _: True) 28 | self.min_input_len = min_input_len 29 | self.expanduser = expanduser 30 | 31 | def get_completions(self, document: Document, 32 | complete_event: CompleteEvent) -> Iterable[Completion]: 33 | text = document.text_before_cursor 34 | 35 | # Complete only when we have at least the minimal input length, 36 | # otherwise, we can too many results and autocompletion will become too 37 | # heavy. 38 | if len(text) < self.min_input_len: 39 | return 40 | 41 | try: 42 | #print('Called!') 43 | # Do tilde expansion. 44 | #if self.expanduser: 45 | # text = os.path.expanduser(text) 46 | 47 | # Directories where to look. 48 | dirnames = self.get_current_dirs() 49 | #print(text) 50 | for dirname in dirnames: 51 | if dirname.startswith(text): 52 | completion = dirname[len(text):] 53 | yield Completion(completion, 0, display=dirname) 54 | #elif dirname.find('=') != -1: 55 | # m = dirname.find('=') + 1 56 | # if dirname[m:].startswith(text): 57 | # completion = dirname 58 | # yield Completion(completion, 0, display=dirname) 59 | except Exception as e: 60 | traceback.print_exc() -------------------------------------------------------------------------------- /msldap/external/Readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This folder contains packages that the current project uses but either needed to be modified and/or the author of the package was not responsive to fix a breaking issue (eg. a newer version of the dependency of the 3rd party package was updated but not the 3rd party package itself, therefore breaks everything.) 4 | Licenses are included in each folder -------------------------------------------------------------------------------- /msldap/external/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/__init__.py -------------------------------------------------------------------------------- /msldap/external/aiocmd/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dor Green 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 | -------------------------------------------------------------------------------- /msldap/external/aiocmd/README.md: -------------------------------------------------------------------------------- 1 | # aiocmd 2 | Coroutine-based CLI generator using prompt_toolkit, similarly to the built-in cmd module. 3 | 4 | ## How to install? 5 | Simply use `pip3 install aiocmd` 6 | 7 | ## How to use? 8 | To use, inherit from the `PromptToolkitCmd` class and implement the `do_` for each command. 9 | 10 | Each command can receive arguments and optional (keyword) arguments. You then must run the `run()` coroutine to start the CLI. 11 | 12 | For instance: 13 | ```python 14 | import asyncio 15 | 16 | from aiocmd import aiocmd 17 | 18 | 19 | class MyCLI(aiocmd.PromptToolkitCmd): 20 | 21 | def do_my_action(self): 22 | """This will appear in help text""" 23 | print("You ran my action!") 24 | 25 | def do_add(self, x, y): 26 | print(int(x) + int(y)) 27 | 28 | async def do_sleep(self, sleep_time=1): 29 | await asyncio.sleep(int(sleep_time)) 30 | 31 | 32 | if __name__ == "__main__": 33 | asyncio.get_event_loop().run_until_complete(MyCLI().run()) 34 | ``` 35 | 36 | Will create this CLI: 37 | 38 | ![CLIImage](./docs/image1.png) 39 | 40 | ## Extra features 41 | 42 | You can implement a custom completion for each command by implementing `__completions`. 43 | 44 | For example, to complete a single-digit number for the `add` action: 45 | 46 | ```python 47 | class MyCLI(aiocmd.PromptToolkitCmd): 48 | 49 | def _add_completions(self): 50 | return WordCompleter([str(i) for i in range(9)]) 51 | ``` 52 | 53 | ![CLIImage](./docs/image2.png) 54 | 55 | You can also set a custom `prompt` and `aliases` parameters for the class (example in docs). 56 | -------------------------------------------------------------------------------- /msldap/external/aiocmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/aiocmd/__init__.py -------------------------------------------------------------------------------- /msldap/external/aiocmd/aiocmd/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/aiocmd/aiocmd/__init__.py -------------------------------------------------------------------------------- /msldap/external/aiocmd/aiocmd/aiocmd.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import inspect 3 | import shlex 4 | import signal 5 | import sys 6 | 7 | from prompt_toolkit import PromptSession 8 | from prompt_toolkit.completion import WordCompleter 9 | from prompt_toolkit.key_binding import KeyBindings 10 | from prompt_toolkit.patch_stdout import patch_stdout 11 | 12 | try: 13 | from prompt_toolkit.completion.nested import NestedCompleter 14 | except ImportError: 15 | from aiocmd.nested_completer import NestedCompleter 16 | 17 | 18 | class ExitPromptException(Exception): 19 | pass 20 | 21 | 22 | class PromptToolkitCmd: 23 | """Baseclass for custom CLIs 24 | 25 | Works similarly to the built-in Cmd class. You can inherit from this class and implement: 26 | - do_ - This will add the "" command to the cli. 27 | The method may receive arguments (required) and keyword arguments (optional). 28 | - __completions - Returns a custom Completer class to use as a completer for this action. 29 | Additionally, the user cant change the "prompt" variable to change how the prompt looks, and add 30 | command aliases to the 'aliases' dict. 31 | """ 32 | ATTR_START = "do_" 33 | prompt = "$ " 34 | doc_header = "Documented commands:" 35 | aliases = {"?": "help", "exit": "quit"} 36 | 37 | def __init__(self, ignore_sigint=True): 38 | self.completer = self._make_completer() 39 | self.session = None 40 | self._ignore_sigint = ignore_sigint 41 | self._currently_running_task = None 42 | 43 | async def run(self): 44 | if self._ignore_sigint and sys.platform != "win32": 45 | asyncio.get_event_loop().add_signal_handler(signal.SIGINT, self._sigint_handler) 46 | self.session = PromptSession(enable_history_search=True, key_bindings=self._get_bindings()) 47 | try: 48 | with patch_stdout(): 49 | await self._run_prompt_forever() 50 | finally: 51 | if self._ignore_sigint and sys.platform != "win32": 52 | asyncio.get_event_loop().remove_signal_handler(signal.SIGINT) 53 | self._on_close() 54 | 55 | async def _run_prompt_forever(self): 56 | while True: 57 | try: 58 | result = await self.session.prompt_async(self.prompt, completer=self.completer) 59 | except EOFError: 60 | return 61 | 62 | if not result: 63 | continue 64 | args = shlex.split(result) 65 | if args[0] in self.command_list: 66 | try: 67 | self._currently_running_task = asyncio.ensure_future( 68 | self._run_single_command(args[0], args[1:])) 69 | await self._currently_running_task 70 | except asyncio.CancelledError: 71 | print() 72 | continue 73 | except ExitPromptException: 74 | return 75 | else: 76 | print("Command %s not found!" % args[0]) 77 | 78 | def _sigint_handler(self): 79 | if self._currently_running_task: 80 | self._currently_running_task.cancel() 81 | 82 | def _get_bindings(self): 83 | bindings = KeyBindings() 84 | bindings.add("c-c")(lambda event: self._interrupt_handler(event)) 85 | return bindings 86 | 87 | async def _run_single_command(self, command, args): 88 | command_real_args, command_real_kwargs = self._get_command_args(command) 89 | if len(args) < len(command_real_args) or len(args) > (len(command_real_args) 90 | + len(command_real_kwargs)): 91 | print("Bad command args. Usage: %s" % self._get_command_usage(command, command_real_args, 92 | command_real_kwargs)) 93 | return 94 | 95 | try: 96 | com_func = self._get_command(command) 97 | if asyncio.iscoroutinefunction(com_func): 98 | res = await com_func(*args) 99 | else: 100 | res = com_func(*args) 101 | return res 102 | except (ExitPromptException, asyncio.CancelledError): 103 | raise 104 | except Exception as ex: 105 | print("Command failed: ", ex) 106 | 107 | def _interrupt_handler(self, event): 108 | event.cli.current_buffer.text = "" 109 | 110 | def _make_completer(self): 111 | return NestedCompleter({com: self._completer_for_command(com) for com in self.command_list}) 112 | 113 | def _completer_for_command(self, command): 114 | if not hasattr(self, "_%s_completions" % command): 115 | return WordCompleter([]) 116 | return getattr(self, "_%s_completions" % command)() 117 | 118 | def _get_command(self, command): 119 | if command in self.aliases: 120 | command = self.aliases[command] 121 | return getattr(self, self.ATTR_START + command) 122 | 123 | def _get_command_args(self, command): 124 | args = [param for param in inspect.signature(self._get_command(command)).parameters.values() 125 | if param.default == param.empty] 126 | kwargs = [param for param in inspect.signature(self._get_command(command)).parameters.values() 127 | if param.default != param.empty] 128 | return args, kwargs 129 | 130 | def _get_command_usage(self, command, args, kwargs): 131 | return ("%s %s %s" % (command, 132 | " ".join("<%s>" % arg for arg in args), 133 | " ".join("[%s]" % kwarg for kwarg in kwargs), 134 | )).strip() 135 | 136 | @property 137 | def command_list(self): 138 | return [attr[len(self.ATTR_START):] 139 | for attr in dir(self) if attr.startswith(self.ATTR_START)] + list(self.aliases.keys()) 140 | 141 | def do_help(self): 142 | print() 143 | print(self.doc_header) 144 | print("=" * len(self.doc_header)) 145 | print() 146 | 147 | get_usage = lambda command: self._get_command_usage(command, *self._get_command_args(command)) 148 | max_usage_len = max([len(get_usage(command)) for command in self.command_list]) 149 | for command in sorted(self.command_list): 150 | command_doc = self._get_command(command).__doc__ 151 | print(("%-" + str(max_usage_len + 2) + "s%s") % (get_usage(command), command_doc or "")) 152 | 153 | def do_quit(self): 154 | """Exit the prompt""" 155 | raise ExitPromptException() 156 | 157 | def _on_close(self): 158 | """Optional hook to call on closing the cmd""" 159 | pass 160 | -------------------------------------------------------------------------------- /msldap/external/aiocmd/aiocmd/nested_completer.py: -------------------------------------------------------------------------------- 1 | """ 2 | Nestedcompleter for completion of hierarchical data structures. 3 | """ 4 | from typing import Dict, Iterable, Mapping, Optional, Set, Union 5 | 6 | from prompt_toolkit.completion import CompleteEvent, Completer, Completion 7 | from prompt_toolkit.completion.word_completer import WordCompleter 8 | from prompt_toolkit.document import Document 9 | 10 | __all__ = [ 11 | 'NestedCompleter' 12 | ] 13 | 14 | NestedDict = Mapping[str, Union['NestedDict', Set[str], None, Completer]] 15 | 16 | 17 | class NestedCompleter(Completer): 18 | """ 19 | Completer which wraps around several other completers, and calls any the 20 | one that corresponds with the first word of the input. 21 | By combining multiple `NestedCompleter` instances, we can achieve multiple 22 | hierarchical levels of autocompletion. This is useful when `WordCompleter` 23 | is not sufficient. 24 | If you need multiple levels, check out the `from_nested_dict` classmethod. 25 | """ 26 | def __init__(self, options: Dict[str, Optional[Completer]], 27 | ignore_case: bool = True) -> None: 28 | 29 | self.options = options 30 | self.ignore_case = ignore_case 31 | 32 | def __repr__(self) -> str: 33 | return 'NestedCompleter(%r, ignore_case=%r)' % (self.options, self.ignore_case) 34 | 35 | @classmethod 36 | def from_nested_dict(cls, data: NestedDict) -> 'NestedCompleter': 37 | """ 38 | Create a `NestedCompleter`, starting from a nested dictionary data 39 | structure, like this: 40 | .. code:: 41 | data = { 42 | 'show': { 43 | 'version': None, 44 | 'interfaces': None, 45 | 'clock': None, 46 | 'ip': {'interface': {'brief'}} 47 | }, 48 | 'exit': None 49 | 'enable': None 50 | } 51 | The value should be `None` if there is no further completion at some 52 | point. If all values in the dictionary are None, it is also possible to 53 | use a set instead. 54 | Values in this data structure can be a completers as well. 55 | """ 56 | options = {} 57 | for key, value in data.items(): 58 | if isinstance(value, Completer): 59 | options[key] = value 60 | elif isinstance(value, dict): 61 | options[key] = cls.from_nested_dict(value) 62 | elif isinstance(value, set): 63 | options[key] = cls.from_nested_dict({item: None for item in value}) 64 | else: 65 | assert value is None 66 | options[key] = None 67 | 68 | return cls(options) 69 | 70 | def get_completions(self, document: Document, 71 | complete_event: CompleteEvent) -> Iterable[Completion]: 72 | # Split document. 73 | text = document.text_before_cursor.lstrip() 74 | 75 | # If there is a space, check for the first term, and use a 76 | # subcompleter. 77 | if ' ' in text: 78 | first_term = text.split()[0] 79 | completer = self.options.get(first_term) 80 | 81 | # If we have a sub completer, use this for the completions. 82 | if completer is not None: 83 | remaining_text = document.text[len(first_term):].lstrip() 84 | move_cursor = len(document.text) - len(remaining_text) 85 | 86 | new_document = Document( 87 | remaining_text, 88 | cursor_position=document.cursor_position - move_cursor) 89 | 90 | for c in completer.get_completions(new_document, complete_event): 91 | yield c 92 | 93 | # No space in the input: behave exactly like `WordCompleter`. 94 | else: 95 | completer = WordCompleter(list(self.options.keys()), ignore_case=self.ignore_case) 96 | for c in completer.get_completions(document, complete_event): 97 | yield c 98 | -------------------------------------------------------------------------------- /msldap/external/aiocmd/docs/example.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from prompt_toolkit.completion import WordCompleter 4 | 5 | from aiocmd import aiocmd 6 | 7 | 8 | class MyCLI(aiocmd.PromptToolkitCmd): 9 | 10 | def __init__(self, my_name="My CLI"): 11 | super().__init__() 12 | self.prompt = "%s $ " % my_name 13 | self.aliases["nap"] = "sleep" 14 | 15 | def do_my_action(self): 16 | """This will appear in help text""" 17 | print("You ran my action!") 18 | 19 | def do_add(self, x, y): 20 | print(int(x) + int(y)) 21 | 22 | def do_echo(self, to_echo): 23 | print(to_echo) 24 | 25 | async def do_sleep(self, sleep_time=1): 26 | await asyncio.sleep(int(sleep_time)) 27 | 28 | def _add_completions(self): 29 | return WordCompleter([str(i) for i in range(9)]) 30 | 31 | def _sleep_completions(self): 32 | return WordCompleter([str(i) for i in range(1, 60)]) 33 | 34 | 35 | if __name__ == "__main__": 36 | asyncio.get_event_loop().run_until_complete(MyCLI().run()) 37 | -------------------------------------------------------------------------------- /msldap/external/aiocmd/docs/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/aiocmd/docs/image1.png -------------------------------------------------------------------------------- /msldap/external/aiocmd/docs/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/aiocmd/docs/image2.png -------------------------------------------------------------------------------- /msldap/external/asciitree/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Marc Brinkmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /msldap/external/asciitree/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /msldap/external/asciitree/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: asciitree 3 | Version: 0.3.3 4 | Summary: Draws ASCII trees. 5 | Home-page: http://github.com/mbr/asciitree 6 | Author: Marc Brinkmann 7 | Author-email: git@marcbrinkmann.de 8 | License: MIT 9 | Description: ASCII Trees 10 | =========== 11 | 12 | .. code:: console 13 | 14 | asciitree 15 | +-- sometimes 16 | | +-- you 17 | +-- just 18 | | +-- want 19 | | +-- to 20 | | +-- draw 21 | +-- trees 22 | +-- in 23 | +-- your 24 | +-- terminal 25 | 26 | 27 | .. code:: python 28 | 29 | from asciitree import LeftAligned 30 | from collections import OrderedDict as OD 31 | 32 | tree = { 33 | 'asciitree': OD([ 34 | ('sometimes', 35 | {'you': {}}), 36 | ('just', 37 | {'want': OD([ 38 | ('to', {}), 39 | ('draw', {}), 40 | ])}), 41 | ('trees', {}), 42 | ('in', { 43 | 'your': { 44 | 'terminal': {} 45 | } 46 | }) 47 | ]) 48 | } 49 | 50 | tr = LeftAligned() 51 | print(tr(tree)) 52 | 53 | 54 | Read the documentation at http://pythonhosted.org/asciitree 55 | 56 | Platform: UNKNOWN 57 | -------------------------------------------------------------------------------- /msldap/external/asciitree/README.rst: -------------------------------------------------------------------------------- 1 | ASCII Trees 2 | =========== 3 | 4 | .. code:: console 5 | 6 | asciitree 7 | +-- sometimes 8 | | +-- you 9 | +-- just 10 | | +-- want 11 | | +-- to 12 | | +-- draw 13 | +-- trees 14 | +-- in 15 | +-- your 16 | +-- terminal 17 | 18 | 19 | .. code:: python 20 | 21 | from asciitree import LeftAligned 22 | from collections import OrderedDict as OD 23 | 24 | tree = { 25 | 'asciitree': OD([ 26 | ('sometimes', 27 | {'you': {}}), 28 | ('just', 29 | {'want': OD([ 30 | ('to', {}), 31 | ('draw', {}), 32 | ])}), 33 | ('trees', {}), 34 | ('in', { 35 | 'your': { 36 | 'terminal': {} 37 | } 38 | }) 39 | ]) 40 | } 41 | 42 | tr = LeftAligned() 43 | print(tr(tree)) 44 | 45 | 46 | Read the documentation at http://pythonhosted.org/asciitree 47 | -------------------------------------------------------------------------------- /msldap/external/asciitree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/asciitree/__init__.py -------------------------------------------------------------------------------- /msldap/external/asciitree/asciitree/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .drawing import BoxStyle 5 | from .traversal import DictTraversal 6 | from .util import KeyArgsConstructor 7 | 8 | 9 | class LeftAligned(KeyArgsConstructor): 10 | """Creates a renderer for a left-aligned tree. 11 | 12 | Any attributes of the resulting class instances can be set using 13 | constructor arguments.""" 14 | 15 | draw = BoxStyle() 16 | "The draw style used. See :class:`~asciitree.drawing.Style`." 17 | traverse = DictTraversal() 18 | "Traversal method. See :class:`~asciitree.traversal.Traversal`." 19 | 20 | def render(self, node): 21 | """Renders a node. This function is used internally, as it returns 22 | a list of lines. Use :func:`~asciitree.LeftAligned.__call__` instead. 23 | """ 24 | lines = [] 25 | 26 | children = self.traverse.get_children(node) 27 | lines.append(self.draw.node_label(self.traverse.get_text(node))) 28 | 29 | for n, child in enumerate(children): 30 | child_tree = self.render(child) 31 | 32 | if n == len(children) - 1: 33 | # last child does not get the line drawn 34 | lines.append(self.draw.last_child_head(child_tree.pop(0))) 35 | lines.extend(self.draw.last_child_tail(l) 36 | for l in child_tree) 37 | else: 38 | lines.append(self.draw.child_head(child_tree.pop(0))) 39 | lines.extend(self.draw.child_tail(l) 40 | for l in child_tree) 41 | 42 | return lines 43 | 44 | def __call__(self, tree): 45 | """Render the tree into string suitable for console output. 46 | 47 | :param tree: A tree.""" 48 | return '\n'.join(self.render(self.traverse.get_root(tree))) 49 | 50 | 51 | # legacy support below 52 | 53 | from .drawing import Style 54 | from .traversal import Traversal 55 | 56 | 57 | class LegacyStyle(Style): 58 | def node_label(self, text): 59 | return text 60 | 61 | def child_head(self, label): 62 | return ' +--' + label 63 | 64 | def child_tail(self, line): 65 | return ' |' + line 66 | 67 | def last_child_head(self, label): 68 | return ' +--' + label 69 | 70 | def last_child_tail(self, line): 71 | return ' ' + line 72 | 73 | 74 | def draw_tree(node, 75 | child_iter=lambda n: n.children, 76 | text_str=str): 77 | """Support asciitree 0.2 API. 78 | 79 | This function solely exist to not break old code (using asciitree 0.2). 80 | Its use is deprecated.""" 81 | return LeftAligned(traverse=Traversal(get_text=text_str, 82 | get_children=child_iter), 83 | draw=LegacyStyle())(node) 84 | -------------------------------------------------------------------------------- /msldap/external/asciitree/asciitree/drawing.py: -------------------------------------------------------------------------------- 1 | from .util import KeyArgsConstructor 2 | 3 | BOX_LIGHT = { 4 | 'UP_AND_RIGHT': u'\u2514', 5 | 'HORIZONTAL': u'\u2500', 6 | 'VERTICAL': u'\u2502', 7 | 'VERTICAL_AND_RIGHT': u'\u251C', 8 | } #: Unicode box-drawing glyphs, light style 9 | 10 | 11 | BOX_HEAVY = { 12 | 'UP_AND_RIGHT': u'\u2517', 13 | 'HORIZONTAL': u'\u2501', 14 | 'VERTICAL': u'\u2503', 15 | 'VERTICAL_AND_RIGHT': u'\u2523', 16 | } #: Unicode box-drawing glyphs, heavy style 17 | 18 | 19 | BOX_DOUBLE = { 20 | 'UP_AND_RIGHT': u'\u255A', 21 | 'HORIZONTAL': u'\u2550', 22 | 'VERTICAL': u'\u2551', 23 | 'VERTICAL_AND_RIGHT': u'\u2560', 24 | } #: Unicode box-drawing glyphs, double-line style 25 | 26 | 27 | BOX_ASCII = { 28 | 'UP_AND_RIGHT': u'+', 29 | 'HORIZONTAL': u'-', 30 | 'VERTICAL': u'|', 31 | 'VERTICAL_AND_RIGHT': u'+', 32 | } #: Unicode box-drawing glyphs, using only ascii ``|+-`` characters. 33 | 34 | 35 | BOX_BLANK = { 36 | 'UP_AND_RIGHT': u' ', 37 | 'HORIZONTAL': u' ', 38 | 'VERTICAL': u' ', 39 | 'VERTICAL_AND_RIGHT': u' ', 40 | } #: Unicode box-drawing glyphs, using only spaces. 41 | 42 | 43 | class Style(KeyArgsConstructor): 44 | """Rendering style for trees.""" 45 | label_format = u'{}' #: Format for labels. 46 | 47 | def node_label(self, text): 48 | """Render a node text into a label.""" 49 | return self.label_format.format(text) 50 | 51 | def child_head(self, label): 52 | """Render a node label into final output.""" 53 | return label 54 | 55 | def child_tail(self, line): 56 | """Render a node line that is not a label into final output.""" 57 | return line 58 | 59 | def last_child_head(self, label): 60 | """Like :func:`~asciitree.drawing.Style.child_head` but only called 61 | for the last child.""" 62 | return label 63 | 64 | def last_child_tail(self, line): 65 | """Like :func:`~asciitree.drawing.Style.child_tail` but only called 66 | for the last child.""" 67 | return line 68 | 69 | 70 | class BoxStyle(Style): 71 | """A rendering style that uses box draw characters and a common layout.""" 72 | gfx = BOX_ASCII #: Glyhps to use. 73 | label_space = 1 #: Space between glyphs and label. 74 | horiz_len = 2 #: Length of horizontal lines 75 | indent = 1 #: Indent for subtrees 76 | 77 | def child_head(self, label): 78 | return (' ' * self.indent 79 | + self.gfx['VERTICAL_AND_RIGHT'] 80 | + self.gfx['HORIZONTAL'] * self.horiz_len 81 | + ' ' * self.label_space 82 | + label) 83 | 84 | def child_tail(self, line): 85 | return (' ' * self.indent 86 | + self.gfx['VERTICAL'] 87 | + ' ' * self.horiz_len 88 | + line) 89 | 90 | def last_child_head(self, label): 91 | return (' ' * self.indent 92 | + self.gfx['UP_AND_RIGHT'] 93 | + self.gfx['HORIZONTAL'] * self.horiz_len 94 | + ' ' * self.label_space 95 | + label) 96 | 97 | def last_child_tail(self, line): 98 | return (' ' * self.indent 99 | + ' ' * len(self.gfx['VERTICAL']) 100 | + ' ' * self.horiz_len 101 | + line) 102 | -------------------------------------------------------------------------------- /msldap/external/asciitree/asciitree/traversal.py: -------------------------------------------------------------------------------- 1 | from .util import KeyArgsConstructor 2 | 3 | 4 | class Traversal(KeyArgsConstructor): 5 | """Traversal method. 6 | 7 | Used by the tree rendering functions like :class:`~asciitree.LeftAligned`. 8 | """ 9 | def get_children(self, node): 10 | """Return a list of children of a node.""" 11 | raise NotImplementedError 12 | 13 | def get_root(self, tree): 14 | """Return a node representing the tree root from the tree.""" 15 | return tree 16 | 17 | def get_text(self, node): 18 | """Return the text associated with a node.""" 19 | return str(node) 20 | 21 | 22 | class DictTraversal(Traversal): 23 | """Traversal suitable for a dictionary. Keys are tree labels, all values 24 | must be dictionaries as well.""" 25 | def get_children(self, node): 26 | return list(node[1].items()) 27 | 28 | def get_root(self, tree): 29 | return list(tree.items())[0] 30 | 31 | def get_text(self, node): 32 | return node[0] 33 | 34 | 35 | class AttributeTraversal(Traversal): 36 | """Attribute traversal. 37 | 38 | Uses an attribute of a node as its list of children. 39 | """ 40 | attribute = 'children' #: Attribute to use. 41 | 42 | def get_children(self, node): 43 | return getattr(node, self.attribute) 44 | -------------------------------------------------------------------------------- /msldap/external/asciitree/asciitree/util.py: -------------------------------------------------------------------------------- 1 | class KeyArgsConstructor(object): 2 | def __init__(self, **kwargs): 3 | for k, v in kwargs.items(): 4 | setattr(self, k, v) 5 | -------------------------------------------------------------------------------- /msldap/external/asciitree/setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | tag_build = 3 | tag_date = 0 4 | tag_svn_revision = 0 5 | 6 | -------------------------------------------------------------------------------- /msldap/external/asciitree/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | from setuptools import setup, find_packages 7 | 8 | 9 | def read(fname): 10 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 11 | 12 | 13 | setup( 14 | name='asciitree', 15 | version='0.3.3', 16 | description='Draws ASCII trees.', 17 | long_description=read('README.rst'), 18 | author='Marc Brinkmann', 19 | author_email='git@marcbrinkmann.de', 20 | url='http://github.com/mbr/asciitree', 21 | license='MIT', 22 | packages=find_packages(exclude=['tests']), 23 | install_requires=[], 24 | ) 25 | -------------------------------------------------------------------------------- /msldap/external/bloodhoundpy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/bloodhoundpy/__init__.py -------------------------------------------------------------------------------- /msldap/external/bloodhoundpy/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/external/bloodhoundpy/lib/__init__.py -------------------------------------------------------------------------------- /msldap/external/bloodhoundpy/resolver.py: -------------------------------------------------------------------------------- 1 | from msldap import logger 2 | 3 | WELLKNOWN_SIDS = { 4 | "S-1-0": ("Null Authority", "USER"), 5 | "S-1-0-0": ("Nobody", "USER"), 6 | "S-1-1": ("World Authority", "USER"), 7 | "S-1-1-0": ("Everyone", "GROUP"), 8 | "S-1-2": ("Local Authority", "USER"), 9 | "S-1-2-0": ("Local", "GROUP"), 10 | "S-1-2-1": ("Console Logon", "GROUP"), 11 | "S-1-3": ("Creator Authority", "USER"), 12 | "S-1-3-0": ("Creator Owner", "USER"), 13 | "S-1-3-1": ("Creator Group", "GROUP"), 14 | "S-1-3-2": ("Creator Owner Server", "COMPUTER"), 15 | "S-1-3-3": ("Creator Group Server", "COMPUTER"), 16 | "S-1-3-4": ("Owner Rights", "GROUP"), 17 | "S-1-4": ("Non-unique Authority", "USER"), 18 | "S-1-5": ("NT Authority", "USER"), 19 | "S-1-5-1": ("Dialup", "GROUP"), 20 | "S-1-5-2": ("Network", "GROUP"), 21 | "S-1-5-3": ("Batch", "GROUP"), 22 | "S-1-5-4": ("Interactive", "GROUP"), 23 | "S-1-5-6": ("Service", "GROUP"), 24 | "S-1-5-7": ("Anonymous", "GROUP"), 25 | "S-1-5-8": ("Proxy", "GROUP"), 26 | "S-1-5-9": ("Enterprise Domain Controllers", "GROUP"), 27 | "S-1-5-10": ("Principal Self", "USER"), 28 | "S-1-5-11": ("Authenticated Users", "GROUP"), 29 | "S-1-5-12": ("Restricted Code", "GROUP"), 30 | "S-1-5-13": ("Terminal Server Users", "GROUP"), 31 | "S-1-5-14": ("Remote Interactive Logon", "GROUP"), 32 | "S-1-5-15": ("This Organization", "GROUP"), 33 | "S-1-5-17": ("IUSR", "USER"), 34 | "S-1-5-18": ("Local System", "USER"), 35 | "S-1-5-19": ("NT Authority", "USER"), 36 | "S-1-5-20": ("Network Service", "USER"), 37 | "S-1-5-80-0": ("All Services ", "GROUP"), 38 | "S-1-5-32-544": ("Administrators", "GROUP"), 39 | "S-1-5-32-545": ("Users", "GROUP"), 40 | "S-1-5-32-546": ("Guests", "GROUP"), 41 | "S-1-5-32-547": ("Power Users", "GROUP"), 42 | "S-1-5-32-548": ("Account Operators", "GROUP"), 43 | "S-1-5-32-549": ("Server Operators", "GROUP"), 44 | "S-1-5-32-550": ("Print Operators", "GROUP"), 45 | "S-1-5-32-551": ("Backup Operators", "GROUP"), 46 | "S-1-5-32-552": ("Replicators", "GROUP"), 47 | "S-1-5-32-554": ("Pre-Windows 2000 Compatible Access", "GROUP"), 48 | "S-1-5-32-555": ("Remote Desktop Users", "GROUP"), 49 | "S-1-5-32-556": ("Network Configuration Operators", "GROUP"), 50 | "S-1-5-32-557": ("Incoming Forest Trust Builders", "GROUP"), 51 | "S-1-5-32-558": ("Performance Monitor Users", "GROUP"), 52 | "S-1-5-32-559": ("Performance Log Users", "GROUP"), 53 | "S-1-5-32-560": ("Windows Authorization Access Group", "GROUP"), 54 | "S-1-5-32-561": ("Terminal Server License Servers", "GROUP"), 55 | "S-1-5-32-562": ("Distributed COM Users", "GROUP"), 56 | "S-1-5-32-568": ("IIS_IUSRS", "GROUP"), 57 | "S-1-5-32-569": ("Cryptographic Operators", "GROUP"), 58 | "S-1-5-32-573": ("Event Log Readers", "GROUP"), 59 | "S-1-5-32-574": ("Certificate Service DCOM Access", "GROUP"), 60 | "S-1-5-32-575": ("RDS Remote Access Servers", "GROUP"), 61 | "S-1-5-32-576": ("RDS Endpoint Servers", "GROUP"), 62 | "S-1-5-32-577": ("RDS Management Servers", "GROUP"), 63 | "S-1-5-32-578": ("Hyper-V Administrators", "GROUP"), 64 | "S-1-5-32-579": ("Access Control Assistance Operators", "GROUP"), 65 | "S-1-5-32-580": ("Access Control Assistance Operators", "GROUP"), 66 | "S-1-5-32-582": ("Storage Replica Administrators", "GROUP") 67 | } 68 | 69 | def resolve_aces(aces, domainname, domainsid, sidcache): 70 | aces_out = [] 71 | for ace in aces: 72 | out = { 73 | 'RightName': ace['rightname'], 74 | 'IsInherited': ace['inherited'] 75 | } 76 | # Is it a well-known sid? 77 | if ace['sid'] in WELLKNOWN_SIDS: 78 | out['PrincipalSID'] = u'%s-%s' % (domainname.upper(), ace['sid']) 79 | out['PrincipalType'] = WELLKNOWN_SIDS[ace['sid']][1].capitalize() 80 | else: 81 | linkitem = sidcache.get(ace['sid']) 82 | if linkitem is None: 83 | logger.debug('[EXT-BH] Cache miss for %s' % ace['sid']) 84 | entry = { 85 | 'type': 'Base', 86 | 'objectid': ace['sid'] 87 | } 88 | linkitem = { 89 | "ObjectIdentifier": entry['objectid'], 90 | "ObjectType": entry['type'].capitalize() 91 | } 92 | sidcache[ace['sid']] = linkitem 93 | out['PrincipalSID'] = ace['sid'] 94 | out['PrincipalType'] = linkitem['ObjectType'].capitalize() 95 | 96 | for tace in aces_out: 97 | if out['PrincipalSID'] == tace['PrincipalSID'] and out['PrincipalType'] == tace['PrincipalType'] and out['RightName'] == tace['RightName'] and out['IsInherited'] == tace['IsInherited']: 98 | break 99 | else: 100 | aces_out.append(out) 101 | return aces_out 102 | 103 | def resolve_sid(sid, domainname, domainsid, sidcache): 104 | # Resolve SIDs for SID history purposes 105 | out = {} 106 | # Is it a well-known sid? 107 | if sid in WELLKNOWN_SIDS: 108 | out['ObjectIdentifier'] = u'%s-%s' % (domainname.upper(), sid) 109 | out['ObjectType'] = WELLKNOWN_SIDS[sid][1].capitalize() 110 | else: 111 | try: 112 | linkitem = sidcache.get(sid) 113 | except KeyError: 114 | # Look it up instead 115 | # Is this SID part of the current domain? If not, use GC 116 | #use_gc = not sid.startswith(domainsid) 117 | #ldapentry = self.resolver.resolve_sid(sid, use_gc) 118 | # Couldn't resolve... 119 | #if not ldapentry: 120 | # logger.debug('Could not resolve SID: %s', sid) 121 | # # Fake it 122 | # entry = { 123 | # 'type': 'Base', 124 | # 'objectid':sid 125 | # } 126 | #else: 127 | # entry = ADUtils.resolve_ad_entry(ldapentry) 128 | entry = { 129 | 'type': 'Base', 130 | 'objectid':sid 131 | } 132 | linkitem = { 133 | "ObjectIdentifier": entry['objectid'], 134 | "ObjectType": entry['type'].capitalize() 135 | } 136 | # Entries are cached regardless of validity - unresolvable sids 137 | # are not likely to be resolved the second time and this saves traffic 138 | sidcache.put(sid, linkitem) 139 | out['ObjectIdentifier'] = sid 140 | out['ObjectType'] = linkitem['ObjectType'] 141 | return out 142 | -------------------------------------------------------------------------------- /msldap/external/bloodhoundpy/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | def reverse_dn_components(dn:str): 5 | rdns = ','.join(reversed(dn.split(','))) 6 | return rdns.upper() 7 | 8 | def explode_dn(dn): 9 | parts = [] 10 | esc = False 11 | part = '' 12 | 13 | for char in dn: 14 | if esc: 15 | part += char 16 | esc = False 17 | elif char == '\\': 18 | esc = True 19 | part += char 20 | elif char == ',': 21 | if part: 22 | parts.append(part) 23 | part = '' 24 | else: 25 | part += char 26 | 27 | if part: 28 | parts.append(part) 29 | 30 | return parts 31 | 32 | 33 | def parse_gplink_string(linkstr): 34 | if not linkstr: 35 | return 36 | for links in linkstr.split('[LDAP://')[1:]: 37 | dn, options = links.rstrip('][').split(';') 38 | yield dn, int(options) 39 | 40 | 41 | 42 | #taken from bloodhound.py 43 | def is_filtered_container(containerdn): 44 | if "CN=DOMAINUPDATES,CN=SYSTEM,DC=" in containerdn.upper(): 45 | return True 46 | if "CN=POLICIES,CN=SYSTEM,DC=" in containerdn.upper() and (containerdn.upper().startswith('CN=USER') or containerdn.upper().startswith('CN=MACHINE')): 47 | return True 48 | return False 49 | 50 | def is_filtered_container_child(containerdn): 51 | if "CN=PROGRAM DATA,DC=" in containerdn.upper(): 52 | return True 53 | if "CN=SYSTEM,DC=" in containerdn.upper(): 54 | return True 55 | return False -------------------------------------------------------------------------------- /msldap/ldap_objects/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | from msldap.ldap_objects.adinfo import MSADInfo, MSADInfo_ATTRS 8 | from msldap.ldap_objects.aduser import MSADUser, MSADUser_ATTRS, MSADUser_TSV_ATTRS 9 | from msldap.ldap_objects.adcomp import MSADMachine, MSADMachine_ATTRS, MSADMachine_TSV_ATTRS 10 | from msldap.ldap_objects.adsec import MSADSecurityInfo, MSADTokenGroup 11 | from msldap.ldap_objects.common import MSLDAP_UAC 12 | from msldap.ldap_objects.adgroup import MSADGroup, MSADGroup_ATTRS 13 | from msldap.ldap_objects.adou import MSADOU, MSADOU_ATTRS 14 | from msldap.ldap_objects.adgpo import MSADGPO, MSADGPO_ATTRS 15 | from msldap.ldap_objects.adtrust import MSADDomainTrust, MSADDomainTrust_ATTRS 16 | from msldap.ldap_objects.adschemaentry import MSADSCHEMAENTRY_ATTRS, MSADSchemaEntry 17 | from msldap.ldap_objects.adca import MSADCA, MSADCA_ATTRS 18 | from msldap.ldap_objects.adenrollmentservice import MSADEnrollmentService_ATTRS, MSADEnrollmentService 19 | from msldap.ldap_objects.adcertificatetemplate import MSADCertificateTemplate, MSADCertificateTemplate_ATTRS 20 | from msldap.ldap_objects.adgmsa import MSADGMSAUser, MSADGMSAUser_ATTRS 21 | from msldap.ldap_objects.adcontainer import MSADContainer, MSADContainer_ATTRS 22 | from msldap.ldap_objects.addmsa import MSADDMSAUser, MSADDMSAUser_ATTRS, MSADDMSAUser_TSV_ATTRS 23 | 24 | 25 | __all__ = [ 26 | 'MSADUser', 27 | 'MSADUser_ATTRS', 28 | 'MSADUser_TSV_ATTRS', 29 | 'MSADInfo', 30 | 'MSADInfo_ATTRS', 31 | 'MSLDAP_UAC', 32 | 'MSADMachine', 33 | 'MSADMachine_ATTRS', 34 | 'MSADMachine_TSV_ATTRS', 35 | 'MSADSecurityInfo', 36 | 'MSADTokenGroup', 37 | 'MSADGroup', 38 | 'MSADOU', 39 | 'MSADGPO', 40 | 'MSADGPO_ATTRS', 41 | 'MSADDomainTrust', 42 | 'MSADDomainTrust_ATTRS', 43 | 'MSADGroup_ATTRS', 44 | 'MSADOU_ATTRS', 45 | 'MSADSCHEMAENTRY_ATTRS', 46 | 'MSADSchemaEntry', 47 | 'MSADCA', 48 | 'MSADCA_ATTRS', 49 | 'MSADEnrollmentService_ATTRS', 50 | 'MSADEnrollmentService', 51 | 'MSADCertificateTemplate', 52 | 'MSADCertificateTemplate_ATTRS', 53 | 'MSADGMSAUser', 54 | 'MSADGMSAUser_ATTRS', 55 | 'MSADContainer', 56 | 'MSADContainer_ATTRS', 57 | 'MSADDMSAUser', 58 | 'MSADDMSAUser_ATTRS', 59 | 'MSADDMSAUser_TSV_ATTRS', 60 | ] -------------------------------------------------------------------------------- /msldap/ldap_objects/adca.py: -------------------------------------------------------------------------------- 1 | 2 | from asn1crypto.x509 import Certificate 3 | 4 | MSADCA_ATTRS = ['cACertificate', 'cn', 'sn', 'distinguishedName', 'whenChanged', 'whenCreated', 'name'] 5 | 6 | class MSADCA: 7 | def __init__(self): 8 | self.location = None 9 | self.sn = None #str 10 | self.cn = None #str 11 | self.distinguishedName = None #dn 12 | self.whenChanged = None 13 | self.whenCreated = None 14 | self.cACertificate = None 15 | self.name = None 16 | 17 | @staticmethod 18 | def from_ldap(entry, location): 19 | adi = MSADCA() 20 | adi.location = location 21 | adi.sn = entry['attributes'].get('sn') 22 | adi.cn = entry['attributes'].get('cn') 23 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 24 | adi.whenChanged = entry['attributes'].get('whenChanged') 25 | adi.whenCreated = entry['attributes'].get('whenCreated') 26 | adi.cACertificate = entry['attributes'].get('cACertificate') 27 | if adi.cACertificate is not None: 28 | adi.cACertificate = Certificate.load(adi.cACertificate) 29 | adi.name = entry['attributes'].get('name') 30 | 31 | return adi 32 | 33 | def __str__(self): 34 | t = '== MSADCA ==\r\n' 35 | for k in self.__dict__: 36 | t += '%s: %s\r\n' % (k, self.__dict__[k]) 37 | 38 | return t 39 | -------------------------------------------------------------------------------- /msldap/ldap_objects/adcontainer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | import base64 7 | from msldap.commons.utils import bh_dt_convert 8 | 9 | 10 | 11 | MSADContainer_ATTRS = [ 12 | 'distinguishedName', 'name', 'objectGUID', 'isCriticalSystemObject','objectClass', 'objectCategory', 13 | 'isDeleted', 'description', 'whenCreated' 14 | ] 15 | 16 | class MSADContainer: 17 | def __init__(self): 18 | self.distinguishedName = None #datetime 19 | self.isCriticalSystemObject = None #int 20 | self.name = None #int 21 | self.objectCategory = None #int 22 | self.objectClass = None #str 23 | self.objectGUID = None #int 24 | self.isDeleted = None #str 25 | self.description = None 26 | self.whenCreated = None #datetime 27 | 28 | @staticmethod 29 | def from_ldap(entry): 30 | adi = MSADContainer() 31 | adi.distinguishedName = entry['attributes'].get('distinguishedName') #datetime 32 | adi.isCriticalSystemObject = entry['attributes'].get('isCriticalSystemObject') #int 33 | adi.name = entry['attributes'].get('name') #str 34 | adi.objectCategory = entry['attributes'].get('objectCategory') #str 35 | adi.objectClass = entry['attributes'].get('objectClass') #str 36 | adi.objectGUID = entry['attributes'].get('objectGUID') #str 37 | adi.isDeleted = entry['attributes'].get('isDeleted') 38 | adi.description = entry['attributes'].get('description') 39 | adi.whenCreated = entry['attributes'].get('whenCreated') 40 | return adi 41 | 42 | def to_dict(self): 43 | d = {} 44 | d['distinguishedName'] = self.distinguishedName 45 | d['isCriticalSystemObject'] = self.isCriticalSystemObject 46 | d['name'] = self.name 47 | d['objectCategory'] = self.objectCategory 48 | d['objectClass'] = self.objectClass 49 | d['objectGUID'] = self.objectGUID 50 | d['isDeleted'] = self.isDeleted 51 | d['description'] = self.description 52 | d['whenCreated'] = self.whenCreated 53 | return d 54 | 55 | def get_row(self, attrs): 56 | t = self.to_dict() 57 | if 'nTSecurityDescriptor' in attrs: 58 | if t['nTSecurityDescriptor'] is not None: 59 | t['nTSecurityDescriptor'] = base64.b64encode(t['nTSecurityDescriptor']).decode() 60 | else: 61 | t['nTSecurityDescriptor'] = b'' 62 | return [str(t.get(x)) for x in attrs] 63 | 64 | def __str__(self): 65 | t = 'MSADContainer\r\n' 66 | d = self.to_dict() 67 | for k in d: 68 | t += '%s: %s\r\n' % (k, d[k]) 69 | return t 70 | 71 | def to_bh(self, domain, domainsid): 72 | return { 73 | 'Aces' : [], 74 | 'ObjectIdentifier' : self.objectGUID.upper(), 75 | "IsDeleted": bool(self.isDeleted), 76 | "IsACLProtected": False , # Post processing 77 | "ChildObjects" : [], #Post processing 78 | 'Properties' : { 79 | 'name' : self.name, 80 | 'domain' : domain, 81 | 'domainsid' : domainsid, 82 | 'distinguishedname' : str(self.distinguishedName).upper(), 83 | 'highvalue' : False, # TODO but seems always false 84 | 'whencreated' : bh_dt_convert(self.whenCreated), 85 | 'description' : self.description , 86 | }, 87 | } -------------------------------------------------------------------------------- /msldap/ldap_objects/addmsa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import datetime #, timedelta, timezone 8 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 9 | 10 | MSADDMSAUser_ATTRS = [ 11 | 'accountExpires', 'badPasswordTime', 'badPwdCount', 'cn', 'codePage', 12 | 'distinguishedName', 'lastLogoff', 'lastLogon', 'lastLogonTimestamp', 13 | 'logonCount', 'name', 'objectCategory', 'objectClass', 'objectGUID', 14 | 'objectSid', 'primaryGroupID', 'pwdLastSet', 'sAMAccountName', 15 | 'sAMAccountType', 'sn', 'userAccountControl', 'whenChanged', 'whenCreated', 16 | 'dNSHostName', 'msDS-SupportedEncryptionTypes', 'msDS-ManagedPasswordId', 17 | 'msDS-ManagedPasswordInterval', 'msDS-GroupMSAMembership', 'msDS-ManagedPassword', 18 | 'msDS-ManagedAccountPrecededByLink', 'msDS-DelegatedMSAState' 19 | ] 20 | MSADDMSAUser_TSV_ATTRS = [ 21 | 'sAMAccountName', 'badPasswordTime', 'badPwdCount', 'pwdLastSet', 'lastLogonTimestamp', 22 | 'whenCreated', 'whenChanged', 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 23 | 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', 'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 24 | 'UAC_DONT_EXPIRE_PASSWD', 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED' 25 | ] 26 | 27 | class MSADDMSAUser: 28 | def __init__(self): 29 | ## ID 30 | self.sn = None #str 31 | self.cn = None #str 32 | self.distinguishedName = None #dn 33 | self.displayName = None #str 34 | self.name = None #str 35 | self.objectCategory = None #dn 36 | self.objectClass = None #str 37 | self.objectGUID = None #uid 38 | self.objectSid = None #str 39 | self.primaryGroupID = None #uid 40 | self.sAMAccountName = None #str 41 | self.dNSHostName = None #str 42 | self.msDS_SupportedEncryptionTypes = None #int 43 | self.msDS_ManagedPasswordId = None #bytes 44 | self.msDS_ManagedPasswordInterval = None #str 45 | self.msDS_GroupMSAMembership = None #SD 46 | self.msDS_ManagedPassword = None #str 47 | self.msDS_ManagedAccountPrecededByLink = None 48 | self.msDS_DelegatedMSAState = None 49 | 50 | ## times 51 | self.accountExpires = None #datetime 52 | self.badPasswordTime = None #datetime 53 | self.lastLogoff = None #datetime 54 | self.lastLogon = None #datetime 55 | self.lastLogonTimestamp = None #datetime 56 | self.pwdLastSet = None #datetime 57 | self.whenChanged = None #datetime 58 | self.whenCreated = None #datetime 59 | 60 | ## security 61 | self.badPwdCount = None #int 62 | self.logonCount = None #int 63 | self.sAMAccountType = None #int 64 | self.userAccountControl = None #UserAccountControl intflag 65 | 66 | ## calculated properties 67 | self.when_pw_change = None #datetime 68 | self.when_pw_expires = None #datetime 69 | self.must_change_pw = None #datetime 70 | 71 | @staticmethod 72 | def from_ldap(entry, adinfo = None): 73 | adi = MSADDMSAUser() 74 | adi.sn = entry['attributes'].get('sn') 75 | adi.cn = entry['attributes'].get('cn') 76 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 77 | adi.name = entry['attributes'].get('name') 78 | adi.objectCategory = entry['attributes'].get('objectCategory') 79 | adi.objectClass = entry['attributes'].get('objectClass') 80 | adi.objectGUID = entry['attributes'].get('objectGUID') 81 | adi.objectSid = entry['attributes'].get('objectSid') 82 | adi.primaryGroupID = entry['attributes'].get('primaryGroupID') 83 | adi.sAMAccountName = entry['attributes'].get('sAMAccountName') 84 | adi.accountExpires = entry['attributes'].get('accountExpires') 85 | adi.badPasswordTime = entry['attributes'].get('badPasswordTime') 86 | adi.lastLogoff = entry['attributes'].get('lastLogoff') 87 | adi.lastLogon = entry['attributes'].get('lastLogon') 88 | adi.lastLogonTimestamp = entry['attributes'].get('lastLogonTimestamp') 89 | adi.pwdLastSet = entry['attributes'].get('pwdLastSet') 90 | adi.whenChanged = entry['attributes'].get('whenChanged') 91 | adi.whenCreated = entry['attributes'].get('whenCreated') 92 | adi.badPwdCount = entry['attributes'].get('badPwdCount') 93 | adi.logonCount = entry['attributes'].get('logonCount') 94 | adi.sAMAccountType = entry['attributes'].get('sAMAccountType') 95 | adi.dNSHostName = entry['attributes'].get('dNSHostName') 96 | adi.msDS_SupportedEncryptionTypes = entry['attributes'].get('msDS-SupportedEncryptionTypes') 97 | adi.msDS_ManagedPasswordId = entry['attributes'].get('msDS-ManagedPasswordId') 98 | adi.msDS_ManagedPasswordInterval = entry['attributes'].get('msDS-ManagedPasswordInterval') 99 | adi.msDS_GroupMSAMembership = entry['attributes'].get('msDS-GroupMSAMembership') 100 | adi.msDS_ManagedPassword = entry['attributes'].get('msDS-ManagedPassword') 101 | adi.msDS_ManagedAccountPrecededByLink = entry['attributes'].get('msDS-ManagedAccountPrecededByLink') 102 | adi.msDS_DelegatedMSAState = entry['attributes'].get('msDS-DelegatedMSAState') 103 | 104 | temp = entry['attributes'].get('userAccountControl') 105 | if temp: 106 | adi.userAccountControl = MSLDAP_UAC(temp) 107 | return adi 108 | 109 | def to_dict(self): 110 | t = {} 111 | t['sn'] = vn(self.sn) 112 | t['cn'] = vn(self.cn) 113 | t['distinguishedName'] = vn(self.distinguishedName) 114 | t['name'] = vn(self.name) 115 | t['objectCategory'] = vn(self.objectCategory) 116 | t['objectClass'] = vn(self.objectClass) 117 | t['objectGUID'] = vn(self.objectGUID) 118 | t['objectSid'] = vn(self.objectSid) 119 | t['primaryGroupID'] = vn(self.primaryGroupID) 120 | t['sAMAccountName'] = vn(self.sAMAccountName) 121 | t['accountExpires'] = vn(self.accountExpires) 122 | t['badPasswordTime'] = vn(self.badPasswordTime) 123 | t['lastLogoff'] = vn(self.lastLogoff) 124 | t['lastLogon'] = vn(self.lastLogon) 125 | t['lastLogonTimestamp'] = vn(self.lastLogonTimestamp) 126 | t['pwdLastSet'] = vn(self.pwdLastSet) 127 | t['whenChanged'] = vn(self.whenChanged) 128 | t['whenCreated'] = vn(self.whenCreated) 129 | t['badPwdCount'] = vn(self.badPwdCount) 130 | t['logonCount'] = vn(self.logonCount) 131 | t['sAMAccountType'] = vn(self.sAMAccountType) 132 | t['userAccountControl'] = vn(self.userAccountControl) 133 | t['dNSHostName'] = vn(self.dNSHostName) 134 | t['msDS_SupportedEncryptionTypes'] = vn(self.msDS_SupportedEncryptionTypes) 135 | t['msDS_ManagedPasswordInterval'] = vn(self.msDS_ManagedPasswordInterval) 136 | t['msDS_GroupMSAMembership'] = vn(self.msDS_GroupMSAMembership) 137 | t['msDS_ManagedPassword'] = vn(self.msDS_ManagedPassword) 138 | t['msDS_ManagedPasswordId'] = vn(self.msDS_ManagedPasswordId) 139 | t['msDS_ManagedAccountPrecededByLink'] = vn(self.msDS_ManagedAccountPrecededByLink) 140 | t['msDS_DelegatedMSAState'] = vn(self.msDS_DelegatedMSAState) 141 | return t 142 | 143 | def uac_to_textflag(self, attr_s): 144 | if self.userAccountControl is None or self.userAccountControl == '': 145 | return 'N/A' 146 | attr = getattr(MSLDAP_UAC, attr_s[4:]) 147 | if self.userAccountControl & attr: 148 | return True 149 | return False 150 | 151 | def get_row(self, attrs): 152 | t = self.to_dict() 153 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] 154 | 155 | def __str__(self): 156 | t = 'MSADGMSAUser\n' 157 | t += 'sn: %s\n' % self.sn 158 | t += 'cn: %s\n' % self.cn 159 | t += 'distinguishedName: %s\n' % self.distinguishedName 160 | t += 'name: %s\n' % self.name 161 | t += 'primaryGroupID: %s\n' % self.primaryGroupID 162 | t += 'sAMAccountName: %s\n' % self.sAMAccountName 163 | t += 'accountExpires: %s\n' % self.accountExpires 164 | t += 'badPasswordTime: %s\n' % self.badPasswordTime 165 | t += 'lastLogoff: %s\n' % self.lastLogoff 166 | t += 'lastLogon: %s\n' % self.lastLogon 167 | t += 'lastLogonTimestamp: %s\n' % self.lastLogonTimestamp 168 | t += 'pwdLastSet: %s\n' % self.pwdLastSet 169 | t += 'whenChanged: %s\n' % self.whenChanged 170 | t += 'whenCreated: %s\n' % self.whenCreated 171 | t += 'objectGUID: %s\n' % self.objectGUID 172 | t += 'objectSid: %s\n' % self.objectSid 173 | t += 'badPwdCount: %s\n' % self.badPwdCount 174 | t += 'logonCount: %s\n' % self.logonCount 175 | t += 'sAMAccountType: %s\n' % self.sAMAccountType 176 | t += 'userAccountControl: %s\n' % self.userAccountControl 177 | t += 'dNSHostName %s\n' % self.dNSHostName 178 | t += 'msDS-SupportedEncryptionTypes: %s\n' % self.msDS_SupportedEncryptionTypes 179 | t += 'msDS-ManagedPasswordId: %s\n' % self.msDS_ManagedPasswordId 180 | t += 'msDS-ManagedPasswordInterval: %s\n' % self.msDS_ManagedPasswordInterval 181 | t += 'msDS-GroupMSAMembership: %s\n' % self.msDS_GroupMSAMembership 182 | t += 'msDS-ManagedPassword: %s\n' % self.msDS_ManagedPassword 183 | return t 184 | -------------------------------------------------------------------------------- /msldap/ldap_objects/adenrollmentservice.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from msldap.commons.utils import print_cert 4 | from asn1crypto.x509 import Certificate 5 | 6 | MSADEnrollmentService_ATTRS = ['cACertificate', 'msPKI-Enrollment-Servers', 'dNSHostName', 'cn', 'sn', 'distinguishedName', 'whenChanged', 'whenCreated', 'name', 'displayName', 'cACertificateDN', 'certificateTemplates'] 7 | 8 | class MSADEnrollmentService: 9 | def __init__(self): 10 | self.sn = None #str 11 | self.cn = None #str 12 | self.distinguishedName = None #dn 13 | self.name = None 14 | self.displayName = None 15 | self.cACertificate = None 16 | self.cACertificateDN = None 17 | self.dNSHostName = None 18 | self.certificateTemplates = [] 19 | self.enrollmentServers = [] 20 | 21 | @staticmethod 22 | def from_ldap(entry): 23 | adi = MSADEnrollmentService() 24 | adi.sn = entry['attributes'].get('sn') 25 | adi.cn = entry['attributes'].get('cn') 26 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 27 | adi.cACertificate = entry['attributes'].get('cACertificate') 28 | if adi.cACertificate is not None: 29 | adi.cACertificate = Certificate.load(adi.cACertificate) 30 | adi.name = entry['attributes'].get('name') 31 | adi.displayName = entry['attributes'].get('displayName') 32 | adi.dNSHostName = entry['attributes'].get('dNSHostName') 33 | adi.cACertificateDN = entry['attributes'].get('cACertificateDN') 34 | adi.certificateTemplates = entry['attributes'].get('certificateTemplates', []) 35 | for serverdef in entry['attributes'].get('msPKI-Enrollment-Servers', []): 36 | adi.enrollmentServers.append(serverdef.split('\n')[3]) 37 | return adi 38 | 39 | def __str__(self): 40 | t = '== MSADEnrollmentService ==\r\n' 41 | t += "Name: %s\r\n" % self.name 42 | t += "DNS name: %s\r\n" % self.dNSHostName 43 | t += "Templates: %s\r\n" % ', '.join(self.certificateTemplates) 44 | if len(self.enrollmentServers) > 0: 45 | t += "Web services: %s\r\n" % ", ".join(self.enrollmentServers) 46 | t += "Certificate: \r\n%s\r\n" % print_cert(self.cACertificate.native, 2) 47 | 48 | return t 49 | -------------------------------------------------------------------------------- /msldap/ldap_objects/adgmsa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import datetime #, timedelta, timezone 8 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 9 | 10 | MSADGMSAUser_ATTRS = [ 11 | 'accountExpires', 'badPasswordTime', 'badPwdCount', 'cn', 'codePage', 12 | 'distinguishedName', 'lastLogoff', 'lastLogon', 'lastLogonTimestamp', 13 | 'logonCount', 'name', 'objectCategory', 'objectClass', 'objectGUID', 14 | 'objectSid', 'primaryGroupID', 'pwdLastSet', 'sAMAccountName', 15 | 'sAMAccountType', 'sn', 'userAccountControl', 'whenChanged', 'whenCreated', 16 | 'dNSHostName', 'msDS-SupportedEncryptionTypes', 'msDS-ManagedPasswordId', 17 | 'msDS-ManagedPasswordInterval', 'msDS-GroupMSAMembership', 'msDS-ManagedPassword' 18 | ] 19 | MSADGMSAUser_TSV_ATTRS = [ 20 | 'sAMAccountName', 'badPasswordTime', 'badPwdCount', 'pwdLastSet', 'lastLogonTimestamp', 21 | 'whenCreated', 'whenChanged', 'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 22 | 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', 'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 23 | 'UAC_DONT_EXPIRE_PASSWD', 'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED' 24 | ] 25 | 26 | class MSADGMSAUser: 27 | def __init__(self): 28 | ## ID 29 | self.sn = None #str 30 | self.cn = None #str 31 | self.distinguishedName = None #dn 32 | self.displayName = None #str 33 | self.name = None #str 34 | self.objectCategory = None #dn 35 | self.objectClass = None #str 36 | self.objectGUID = None #uid 37 | self.objectSid = None #str 38 | self.primaryGroupID = None #uid 39 | self.sAMAccountName = None #str 40 | self.dNSHostName = None #str 41 | self.msDS_SupportedEncryptionTypes = None #int 42 | self.msDS_ManagedPasswordId = None #bytes 43 | self.msDS_ManagedPasswordInterval = None #str 44 | self.msDS_GroupMSAMembership = None #SD 45 | self.msDS_ManagedPassword = None #str 46 | 47 | ## times 48 | self.accountExpires = None #datetime 49 | self.badPasswordTime = None #datetime 50 | self.lastLogoff = None #datetime 51 | self.lastLogon = None #datetime 52 | self.lastLogonTimestamp = None #datetime 53 | self.pwdLastSet = None #datetime 54 | self.whenChanged = None #datetime 55 | self.whenCreated = None #datetime 56 | 57 | ## security 58 | self.badPwdCount = None #int 59 | self.logonCount = None #int 60 | self.sAMAccountType = None #int 61 | self.userAccountControl = None #UserAccountControl intflag 62 | 63 | ## calculated properties 64 | self.when_pw_change = None #datetime 65 | self.when_pw_expires = None #datetime 66 | self.must_change_pw = None #datetime 67 | 68 | @staticmethod 69 | def from_ldap(entry, adinfo = None): 70 | adi = MSADGMSAUser() 71 | adi.sn = entry['attributes'].get('sn') 72 | adi.cn = entry['attributes'].get('cn') 73 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 74 | adi.name = entry['attributes'].get('name') 75 | adi.objectCategory = entry['attributes'].get('objectCategory') 76 | adi.objectClass = entry['attributes'].get('objectClass') 77 | adi.objectGUID = entry['attributes'].get('objectGUID') 78 | adi.objectSid = entry['attributes'].get('objectSid') 79 | adi.primaryGroupID = entry['attributes'].get('primaryGroupID') 80 | adi.sAMAccountName = entry['attributes'].get('sAMAccountName') 81 | adi.accountExpires = entry['attributes'].get('accountExpires') 82 | adi.badPasswordTime = entry['attributes'].get('badPasswordTime') 83 | adi.lastLogoff = entry['attributes'].get('lastLogoff') 84 | adi.lastLogon = entry['attributes'].get('lastLogon') 85 | adi.lastLogonTimestamp = entry['attributes'].get('lastLogonTimestamp') 86 | adi.pwdLastSet = entry['attributes'].get('pwdLastSet') 87 | adi.whenChanged = entry['attributes'].get('whenChanged') 88 | adi.whenCreated = entry['attributes'].get('whenCreated') 89 | adi.badPwdCount = entry['attributes'].get('badPwdCount') 90 | adi.logonCount = entry['attributes'].get('logonCount') 91 | adi.sAMAccountType = entry['attributes'].get('sAMAccountType') 92 | adi.dNSHostName = entry['attributes'].get('dNSHostName') 93 | adi.msDS_SupportedEncryptionTypes = entry['attributes'].get('msDS-SupportedEncryptionTypes') 94 | adi.msDS_ManagedPasswordId = entry['attributes'].get('msDS-ManagedPasswordId') 95 | adi.msDS_ManagedPasswordInterval = entry['attributes'].get('msDS-ManagedPasswordInterval') 96 | adi.msDS_GroupMSAMembership = entry['attributes'].get('msDS-GroupMSAMembership') 97 | adi.msDS_ManagedPassword = entry['attributes'].get('msDS-ManagedPassword') 98 | 99 | temp = entry['attributes'].get('userAccountControl') 100 | if temp: 101 | adi.userAccountControl = MSLDAP_UAC(temp) 102 | return adi 103 | 104 | def to_dict(self): 105 | t = {} 106 | t['sn'] = vn(self.sn) 107 | t['cn'] = vn(self.cn) 108 | t['distinguishedName'] = vn(self.distinguishedName) 109 | t['name'] = vn(self.name) 110 | t['objectCategory'] = vn(self.objectCategory) 111 | t['objectClass'] = vn(self.objectClass) 112 | t['objectGUID'] = vn(self.objectGUID) 113 | t['objectSid'] = vn(self.objectSid) 114 | t['primaryGroupID'] = vn(self.primaryGroupID) 115 | t['sAMAccountName'] = vn(self.sAMAccountName) 116 | t['accountExpires'] = vn(self.accountExpires) 117 | t['badPasswordTime'] = vn(self.badPasswordTime) 118 | t['lastLogoff'] = vn(self.lastLogoff) 119 | t['lastLogon'] = vn(self.lastLogon) 120 | t['lastLogonTimestamp'] = vn(self.lastLogonTimestamp) 121 | t['pwdLastSet'] = vn(self.pwdLastSet) 122 | t['whenChanged'] = vn(self.whenChanged) 123 | t['whenCreated'] = vn(self.whenCreated) 124 | t['badPwdCount'] = vn(self.badPwdCount) 125 | t['logonCount'] = vn(self.logonCount) 126 | t['sAMAccountType'] = vn(self.sAMAccountType) 127 | t['userAccountControl'] = vn(self.userAccountControl) 128 | t['dNSHostName'] = vn(self.dNSHostName) 129 | t['msDS_SupportedEncryptionTypes'] = vn(self.msDS_SupportedEncryptionTypes) 130 | t['msDS_ManagedPasswordInterval'] = vn(self.msDS_ManagedPasswordInterval) 131 | t['msDS_GroupMSAMembership'] = vn(self.msDS_GroupMSAMembership) 132 | t['msDS_ManagedPassword'] = vn(self.msDS_ManagedPassword) 133 | t['msDS_ManagedPasswordId'] = vn(self.msDS_ManagedPasswordId) 134 | return t 135 | 136 | def uac_to_textflag(self, attr_s): 137 | if self.userAccountControl is None or self.userAccountControl == '': 138 | return 'N/A' 139 | attr = getattr(MSLDAP_UAC, attr_s[4:]) 140 | if self.userAccountControl & attr: 141 | return True 142 | return False 143 | 144 | def get_row(self, attrs): 145 | t = self.to_dict() 146 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] 147 | 148 | def __str__(self): 149 | t = 'MSADGMSAUser\n' 150 | t += 'sn: %s\n' % self.sn 151 | t += 'cn: %s\n' % self.cn 152 | t += 'distinguishedName: %s\n' % self.distinguishedName 153 | t += 'name: %s\n' % self.name 154 | t += 'primaryGroupID: %s\n' % self.primaryGroupID 155 | t += 'sAMAccountName: %s\n' % self.sAMAccountName 156 | t += 'accountExpires: %s\n' % self.accountExpires 157 | t += 'badPasswordTime: %s\n' % self.badPasswordTime 158 | t += 'lastLogoff: %s\n' % self.lastLogoff 159 | t += 'lastLogon: %s\n' % self.lastLogon 160 | t += 'lastLogonTimestamp: %s\n' % self.lastLogonTimestamp 161 | t += 'pwdLastSet: %s\n' % self.pwdLastSet 162 | t += 'whenChanged: %s\n' % self.whenChanged 163 | t += 'whenCreated: %s\n' % self.whenCreated 164 | t += 'objectGUID: %s\n' % self.objectGUID 165 | t += 'objectSid: %s\n' % self.objectSid 166 | t += 'badPwdCount: %s\n' % self.badPwdCount 167 | t += 'logonCount: %s\n' % self.logonCount 168 | t += 'sAMAccountType: %s\n' % self.sAMAccountType 169 | t += 'userAccountControl: %s\n' % self.userAccountControl 170 | t += 'dNSHostName %s\n' % self.dNSHostName 171 | t += 'msDS-SupportedEncryptionTypes: %s\n' % self.msDS_SupportedEncryptionTypes 172 | t += 'msDS-ManagedPasswordId: %s\n' % self.msDS_ManagedPasswordId 173 | t += 'msDS-ManagedPasswordInterval: %s\n' % self.msDS_ManagedPasswordInterval 174 | t += 'msDS-GroupMSAMembership: %s\n' % self.msDS_GroupMSAMembership 175 | t += 'msDS-ManagedPassword: %s\n' % self.msDS_ManagedPassword 176 | return t 177 | -------------------------------------------------------------------------------- /msldap/ldap_objects/adgpo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 8 | from msldap.commons.utils import bh_dt_convert 9 | 10 | 11 | MSADGPO_ATTRS = [ 12 | 'cn', 'displayName', 'distinguishedName', 'flags', 'gPCFileSysPath', 13 | 'gPCFunctionalityVersion', 'gPCMachineExtensionNames', 'gPCUserExtensionNames', 14 | 'objectClass', 'objectGUID', 'systemFlags', 'versionNumber', 'whenChanged', 15 | 'whenCreated', 'isDeleted', 'description' 16 | ] 17 | 18 | class MSADGPO: 19 | def __init__(self): 20 | self.cn = None 21 | self.displayName = None 22 | self.distinguishedName = None 23 | self.flags = None 24 | self.gPCFileSysPath = None #str 25 | self.gPCFunctionalityVersion = None #str 26 | self.gPCMachineExtensionNames = None 27 | self.gPCUserExtensionNames = None 28 | self.objectClass = None #str 29 | self.objectGUID = None #uid 30 | self.systemFlags = None #str 31 | self.whenChanged = None #uid 32 | self.whenCreated = None #str 33 | self.versionNumber = None 34 | self.isDeleted = None 35 | self.description = None 36 | 37 | @staticmethod 38 | def from_ldap(entry, adinfo = None): 39 | adi = MSADGPO() 40 | adi.cn = entry['attributes'].get('cn') 41 | adi.displayName = entry['attributes'].get('displayName') 42 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 43 | adi.flags = entry['attributes'].get('flags') 44 | adi.gPCFileSysPath = entry['attributes'].get('gPCFileSysPath') 45 | adi.gPCFunctionalityVersion = entry['attributes'].get('gPCFunctionalityVersion') 46 | adi.gPCMachineExtensionNames = entry['attributes'].get('gPCMachineExtensionNames') 47 | adi.gPCUserExtensionNames = entry['attributes'].get('gPCUserExtensionNames') 48 | adi.objectClass = entry['attributes'].get('objectClass') 49 | adi.objectGUID = entry['attributes'].get('objectGUID') 50 | adi.systemFlags = entry['attributes'].get('systemFlags') 51 | adi.whenChanged = entry['attributes'].get('whenChanged') 52 | adi.whenCreated = entry['attributes'].get('whenCreated') 53 | adi.versionNumber = entry['attributes'].get('versionNumber') 54 | adi.isDeleted = entry['attributes'].get('isDeleted') 55 | adi.description = entry['attributes'].get('description') 56 | 57 | return adi 58 | 59 | def to_dict(self): 60 | t = {} 61 | t['cn'] = vn(self.cn) 62 | t['displayName'] = vn(self.displayName) 63 | t['distinguishedName'] = vn(self.distinguishedName) 64 | t['flags'] = vn(self.flags) 65 | t['gPCFileSysPath'] = vn(self.gPCFileSysPath) 66 | t['gPCFunctionalityVersion'] = vn(self.gPCFunctionalityVersion) 67 | t['gPCMachineExtensionNames'] = vn(self.gPCMachineExtensionNames) 68 | t['gPCUserExtensionNames'] = vn(self.gPCUserExtensionNames) 69 | t['systemFlags'] = vn(self.systemFlags) 70 | t['objectClass'] = vn(self.objectClass) 71 | t['objectGUID'] = vn(self.objectGUID) 72 | t['whenChanged'] = vn(self.whenChanged) 73 | t['whenCreated'] = vn(self.whenCreated) 74 | t['versionNumber'] = vn(self.versionNumber) 75 | t['isDeleted'] = vn(self.isDeleted) 76 | t['description'] = vn(self.description) 77 | return t 78 | 79 | def get_row(self, attrs): 80 | t = self.to_dict() 81 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] 82 | 83 | def __str__(self): 84 | t = 'MSADUser\n' 85 | t += 'cn: %s\n' % self.cn 86 | t += 'distinguishedName: %s\n' % self.distinguishedName 87 | t += 'path: %s\n' % self.gPCFileSysPath 88 | t += 'displayName: %s\n' % self.displayName 89 | 90 | return t 91 | 92 | def to_bh(self, domain, domainsid): 93 | return { 94 | 'Aces' : [], 95 | 'ObjectIdentifier' : self.objectGUID.upper(), 96 | "IsDeleted": bool(self.isDeleted), 97 | "IsACLProtected": False , # Post processing 98 | 'Properties' : { 99 | 'name' : '%s@%s' % (self.displayName.upper(), domain.upper()), 100 | 'domain' : domain, 101 | 'domainsid' : domainsid, 102 | 'distinguishedname' : str(self.distinguishedName).upper(), 103 | 'highvalue' : False, # TODO seems always false 104 | 'whencreated' : bh_dt_convert(self.whenCreated), 105 | 'description' : self.description, 106 | 'gpcpath' : self.gPCFileSysPath.upper(), 107 | }, 108 | } 109 | -------------------------------------------------------------------------------- /msldap/ldap_objects/adgroup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | from msldap.wintypes import * 8 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 9 | from winacl.dtyp.sid import SID 10 | from msldap.commons.utils import bh_dt_convert 11 | 12 | 13 | MSADGroup_ATTRS = [ 14 | 'cn', 'distinguishedName', 'objectGUID', 'objectSid', 'groupType', 15 | 'instanceType', 'name', 'member', 'sAMAccountName', 'systemFlags', 16 | 'whenChanged', 'whenCreated', 'description', 'nTSecurityDescriptor', 17 | 'sAMAccountType', 'adminCount', 'isDeleted' 18 | ] 19 | 20 | MSADGroup_highvalue = ["S-1-5-32-544", "S-1-5-32-550", "S-1-5-32-549", "S-1-5-32-551", "S-1-5-32-548"] 21 | 22 | 23 | 24 | class MSADGroup: 25 | def __init__(self): 26 | self.cn = None #str 27 | self.distinguishedName = None #dn 28 | self.objectGUID = None 29 | self.objectSid = None 30 | self.description = None 31 | self.groupType = None 32 | self.instanceType = None 33 | self.name = None 34 | self.member = None 35 | self.nTSecurityDescriptor = None 36 | self.sAMAccountName = None 37 | self.sAMAccountType = None 38 | self.systemFlags = None 39 | self.whenChanged = None 40 | self.whenCreated = None 41 | self.adminCount = None 42 | self.isDeleted = None 43 | 44 | def to_dict(self): 45 | d = {} 46 | d['cn'] = self.cn 47 | d['distinguishedName'] = self.distinguishedName 48 | d['objectGUID'] = self.objectGUID 49 | d['objectSid'] = self.objectSid 50 | d['description'] = self.description 51 | d['groupType'] = self.groupType 52 | d['instanceType'] = self.instanceType 53 | d['name'] = self.name 54 | d['member'] = self.member 55 | d['nTSecurityDescriptor'] = self.nTSecurityDescriptor 56 | d['sAMAccountName'] = self.sAMAccountName 57 | d['sAMAccountType'] = self.sAMAccountType 58 | d['systemFlags'] = self.systemFlags 59 | d['whenChanged'] = self.whenChanged 60 | d['whenCreated'] = self.whenCreated 61 | d['adminCount'] = self.adminCount 62 | d['isDeleted'] = self.isDeleted 63 | 64 | return d 65 | 66 | @staticmethod 67 | def from_ldap(entry): 68 | t = MSADGroup() 69 | t.cn = entry['attributes'].get('cn') 70 | t.distinguishedName = entry['attributes'].get('distinguishedName') 71 | t.objectGUID = entry['attributes'].get('objectGUID') 72 | t.objectSid = entry['attributes'].get('objectSid') 73 | t.groupType = entry['attributes'].get('groupType') 74 | t.instanceType = entry['attributes'].get('instanceType') 75 | t.name = entry['attributes'].get('name') 76 | t.member = entry['attributes'].get('member') 77 | t.sAMAccountName = entry['attributes'].get('sAMAccountName') 78 | t.systemFlags = entry['attributes'].get('systemFlags') 79 | t.whenChanged = entry['attributes'].get('whenChanged') 80 | t.whenCreated = entry['attributes'].get('whenCreated') 81 | t.adminCount = entry['attributes'].get('adminCount') 82 | t.isDeleted = entry['attributes'].get('isDeleted') 83 | 84 | t.description = entry['attributes'].get('description') 85 | if isinstance(t.description, list): 86 | if len(t.description) == 1: 87 | t.description = t.description[0] 88 | else: 89 | t.description = ', '.join(t.description) 90 | 91 | 92 | #temp = entry['attributes'].get('nTSecurityDescriptor') 93 | #if temp: 94 | # t.nTSecurityDescriptor = SID.from_bytes(temp) 95 | return t 96 | 97 | def get_row(self, attrs): 98 | t = self.to_dict() 99 | return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs] 100 | 101 | def __str__(self): 102 | t = 'MSADGroup\r\n' 103 | for x in self.__dict__: 104 | if not isinstance(self.__dict__[x], (list, dict)): 105 | t += '%s: %s\r\n' % (x, str(self.__dict__[x])) 106 | else: 107 | t += '%s: %s\r\n' % (x, self.__dict__[x]) 108 | return t 109 | 110 | def to_bh(self, domain): 111 | # Thx Dirk-jan 112 | def is_highvalue(sid:str): 113 | if sid.endswith("-512") or sid.endswith("-516") or sid.endswith("-519") or sid.endswith("-520"): 114 | return True 115 | if sid in MSADGroup_highvalue: 116 | return True 117 | return False 118 | 119 | return { 120 | 'Aces' : [], 121 | 'Members': [], 122 | 'ObjectIdentifier' : self.objectSid, 123 | "IsDeleted": bool(self.isDeleted), 124 | "IsACLProtected": False , # Post processing 125 | 'Properties' : { 126 | 'name' : '%s@%s' % (self.name.upper(), domain.upper()), 127 | 'domain' : domain, 128 | 'domainsid' : str(self.objectSid).rsplit('-',1)[0] , 129 | 'distinguishedname' : str(self.distinguishedName).upper(), 130 | 'highvalue' : is_highvalue(str(self.objectSid)), 131 | 'admincount' : bool(self.adminCount), 132 | 'description' : self.description , 133 | 'samaccountname' : self.sAMAccountName , 134 | 'whencreated' : bh_dt_convert(self.whenCreated), 135 | }, 136 | } -------------------------------------------------------------------------------- /msldap/ldap_objects/adinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | from msldap.commons.utils import bh_dt_convert 8 | from msldap.commons.utils import FUNCTIONAL_LEVELS 9 | 10 | MSADInfo_ATTRS = [ 11 | 'auditingPolicy', 'creationTime', 'dc', 'distinguishedName', 12 | 'forceLogoff', 'instanceType', 'lockoutDuration', 'lockOutObservationWindow', 13 | 'lockoutThreshold', 'masteredBy', 'maxPwdAge', 'minPwdAge', 'minPwdLength', 14 | 'name', 'nextRid', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', 15 | 'objectGUID', 'objectSid', 'pwdHistoryLength', 16 | 'pwdProperties', 'serverState', 'systemFlags', 'uASCompat', 'uSNChanged', 17 | 'uSNCreated', 'whenChanged', 'whenCreated', 'rIDManagerReference', 18 | 'msDS-Behavior-Version', 'description', 'isDeleted', 'gPLink', 'ms-DS-MachineAccountQuota' 19 | ] 20 | 21 | class MSADInfo: 22 | def __init__(self): 23 | self.auditingPolicy = None #dunno 24 | self.creationTime = None #datetime 25 | self.dc = None #str 26 | self.distinguishedName = None #string 27 | self.forceLogoff = None #int 28 | self.instanceType = None #int 29 | self.lockoutDuration = None #int 30 | self.lockOutObservationWindow = None #int 31 | self.lockoutThreshold = None #int 32 | self.masteredBy = None #str 33 | self.maxPwdAge = None #int 34 | self.minPwdAge = None #int 35 | self.minPwdLength = None #int 36 | self.name = None #str 37 | self.nextRid = None #int 38 | self.nTSecurityDescriptor = None #str 39 | self.objectCategory = None #str 40 | self.objectClass = None #str 41 | self.objectGUID = None #str 42 | self.objectSid = None #str 43 | self.pwdHistoryLength = None #int 44 | self.pwdProperties = None #int 45 | self.serverState = None #int 46 | self.systemFlags = None #int 47 | self.uASCompat = None #int 48 | self.uSNChanged = None #int 49 | self.uSNCreated = None #int 50 | self.whenChanged = None #datetime 51 | self.whenCreated = None #datetime 52 | self.rIDManagerReference = None #str 53 | self.domainmodelevel = None 54 | self.description = None 55 | self.isDeleted = None 56 | self.gPLink = None 57 | self.machineAccountQuota = None 58 | 59 | @staticmethod 60 | def from_ldap(entry): 61 | adi = MSADInfo() 62 | adi.auditingPolicy = entry['attributes'].get('auditingPolicy') #dunno 63 | adi.creationTime = entry['attributes'].get('creationTime') #datetime 64 | adi.dc = entry['attributes'].get('dc') #str 65 | adi.distinguishedName = entry['attributes'].get('distinguishedName') #string 66 | adi.forceLogoff = entry['attributes'].get('forceLogoff') #int 67 | adi.instanceType = entry['attributes'].get('instanceType') #int 68 | adi.lockoutDuration = entry['attributes'].get('lockoutDuration') #int 69 | adi.lockOutObservationWindow = entry['attributes'].get('lockOutObservationWindow') #int 70 | adi.lockoutThreshold = entry['attributes'].get('lockoutThreshold') #int 71 | adi.masteredBy = entry['attributes'].get('masteredBy') #str 72 | adi.maxPwdAge = entry['attributes'].get('maxPwdAge') #int 73 | adi.minPwdAge = entry['attributes'].get('minPwdAge') #int 74 | adi.minPwdLength = entry['attributes'].get('minPwdLength') #int 75 | adi.name = entry['attributes'].get('name') #str 76 | adi.nextRid = entry['attributes'].get('nextRid') #int 77 | adi.nTSecurityDescriptor = entry['attributes'].get('nTSecurityDescriptor') #str 78 | adi.objectCategory = entry['attributes'].get('objectCategory') #str 79 | adi.objectClass = entry['attributes'].get('objectClass') #str 80 | adi.objectGUID = entry['attributes'].get('objectGUID') #str 81 | adi.objectSid = entry['attributes'].get('objectSid') #str 82 | adi.pwdHistoryLength = entry['attributes'].get('pwdHistoryLength') #int 83 | adi.pwdProperties = entry['attributes'].get('pwdProperties') #int 84 | adi.serverState = entry['attributes'].get('serverState') #int 85 | adi.systemFlags = entry['attributes'].get('systemFlags') #int 86 | adi.uASCompat = entry['attributes'].get('uASCompat') #int 87 | adi.uSNChanged = entry['attributes'].get('uSNChanged') #int 88 | adi.uSNCreated = entry['attributes'].get('uSNCreated') #int 89 | adi.whenChanged = entry['attributes'].get('whenChanged') #datetime 90 | adi.whenCreated = entry['attributes'].get('whenCreated') #datetime 91 | adi.rIDManagerReference = entry['attributes'].get('rIDManagerReference') 92 | adi.description = entry['attributes'].get('description') 93 | adi.isDeleted = entry['attributes'].get('isDeleted') 94 | adi.gPLink = entry['attributes'].get('gPLink') 95 | adi.machineAccountQuota = entry['attributes'].get('ms-DS-MachineAccountQuota') 96 | 97 | #https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/564dc969-6db3-49b3-891a-f2f8d0a68a7f 98 | adi.domainmodelevel = entry['attributes'].get('msDS-Behavior-Version') 99 | 100 | return adi 101 | 102 | def to_dict(self): 103 | d = {} 104 | d['auditingPolicy'] = self.auditingPolicy 105 | d['creationTime'] = self.creationTime 106 | d['dc'] = self.dc 107 | d['distinguishedName'] = self.distinguishedName 108 | d['forceLogoff'] = self.forceLogoff 109 | d['instanceType'] = self.instanceType 110 | d['lockoutDuration'] = self.lockoutDuration 111 | d['lockOutObservationWindow'] = self.lockOutObservationWindow 112 | d['lockoutThreshold'] = self.lockoutThreshold 113 | d['masteredBy'] = self.masteredBy 114 | d['maxPwdAge'] = self.maxPwdAge 115 | d['minPwdAge'] = self.minPwdAge 116 | d['minPwdLength'] = self.minPwdLength 117 | d['name'] = self.name 118 | d['nextRid'] = self.nextRid 119 | d['nTSecurityDescriptor'] = self.nTSecurityDescriptor 120 | d['objectCategory'] = self.objectCategory 121 | d['objectClass'] = self.objectClass 122 | d['objectGUID'] = self.objectGUID 123 | d['objectSid'] = self.objectSid 124 | d['pwdHistoryLength'] = self.pwdHistoryLength 125 | d['pwdProperties'] = self.pwdProperties 126 | d['serverState'] = self.serverState 127 | d['systemFlags'] = self.systemFlags 128 | d['uASCompat'] = self.uASCompat 129 | d['uSNChanged'] = self.uSNChanged 130 | d['uSNCreated'] = self.uSNCreated 131 | d['whenChanged'] = self.whenChanged 132 | d['whenCreated'] = self.whenCreated 133 | d['domainmodelevel'] = self.domainmodelevel 134 | d['description'] = self.description 135 | d['ms-DS-MachineAccountQuota'] = self.machineAccountQuota 136 | return d 137 | 138 | 139 | def __str__(self): 140 | t = 'MSADInfo\n' 141 | t += 'auditingPolicy: %s\n' % self.auditingPolicy 142 | t += 'creationTime: %s\n' % self.creationTime 143 | t += 'dc: %s\n' % self.dc 144 | t += 'distinguishedName: %s\n' % self.distinguishedName 145 | t += 'forceLogoff: %s\n' % self.forceLogoff 146 | t += 'instanceType: %s\n' % self.instanceType 147 | t += 'lockoutDuration: %s\n' % self.lockoutDuration 148 | t += 'lockOutObservationWindow: %s\n' % self.lockOutObservationWindow 149 | t += 'lockoutThreshold: %s\n' % self.lockoutThreshold 150 | t += 'masteredBy: %s\n' % self.masteredBy 151 | t += 'maxPwdAge: %s\n' % self.maxPwdAge 152 | t += 'minPwdAge: %s\n' % self.minPwdAge 153 | t += 'minPwdLength: %s\n' % self.minPwdLength 154 | t += 'name: %s\n' % self.name 155 | t += 'nextRid: %s\n' % self.nextRid 156 | t += 'nTSecurityDescriptor: %s\n' % self.nTSecurityDescriptor 157 | t += 'objectCategory: %s\n' % self.objectCategory 158 | t += 'objectClass: %s\n' % self.objectClass 159 | t += 'objectGUID: %s\n' % self.objectGUID 160 | t += 'objectSid: %s\n' % self.objectSid 161 | t += 'pwdHistoryLength: %s\n' % self.pwdHistoryLength 162 | t += 'pwdProperties: %s\n' % self.pwdProperties 163 | t += 'serverState: %s\n' % self.serverState 164 | t += 'systemFlags: %s\n' % self.systemFlags 165 | t += 'uASCompat: %s\n' % self.uASCompat 166 | t += 'uSNChanged: %s\n' % self.uSNChanged 167 | t += 'uSNCreated: %s\n' % self.uSNCreated 168 | t += 'whenChanged: %s\n' % self.whenChanged 169 | t += 'whenCreated: %s\n' % self.whenCreated 170 | t += 'domainmodelevel: %s\n' % self.domainmodelevel 171 | t += 'description: %s\n' % self.description 172 | t += 'isDeleted: %s\n' % self.isDeleted 173 | t += 'gPLink: %s\n' % self.gPLink 174 | t += 'ms-DS-MachineAccountQuota: %s\n' % self.machineAccountQuota 175 | return t 176 | 177 | def get_row(self, attrs): 178 | t = self.to_dict() 179 | return [str(t.get(x)) for x in attrs] 180 | 181 | def to_bh(self, domain): 182 | try: 183 | functional_level = FUNCTIONAL_LEVELS[int(self.domainmodelevel)] 184 | except KeyError: 185 | functional_level = 'Unknown' 186 | return { 187 | '_gPLink' : self.gPLink, #always empty 188 | 'Aces' : [], 189 | 'ObjectIdentifier' : str(self.objectSid), 190 | "IsDeleted": bool(self.isDeleted), 191 | "IsACLProtected": False , # Post processing 192 | "Trusts" : [], #Post processing 193 | "ChildObjects": [], #Post processing 194 | "GPOChanges" : { 195 | "LocalAdmins": [], 196 | "RemoteDesktopUsers": [], 197 | "DcomUsers": [], 198 | "PSRemoteUsers": [], 199 | "AffectedComputers": [] 200 | }, 201 | "Links": [], # Post processing 202 | 'Properties' : { 203 | 'name' : domain, 204 | 'domain' : domain, 205 | 'domainsid' : str(self.objectSid).rsplit('-',1)[0] , 206 | 'distinguishedname' : str(self.distinguishedName).upper(), 207 | 'description' : self.description, 208 | 'functionallevel' : functional_level, 209 | 'highvalue' : True, 210 | 'whencreated' : bh_dt_convert(self.whenCreated), 211 | } 212 | 213 | } -------------------------------------------------------------------------------- /msldap/ldap_objects/adou.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | import base64 7 | from msldap.commons.utils import bh_dt_convert 8 | 9 | 10 | 11 | MSADOU_ATTRS = [ 12 | 'description', 'distinguishedName', 'dSCorePropagationData', 'gPLink', 'instanceType', 13 | 'isCriticalSystemObject', 'name', 'nTSecurityDescriptor', 'objectCategory', 'objectClass', 14 | 'objectGUID', 'ou', 'showInAdvancedViewOnly', 'systemFlags', 'uSNChanged', 'uSNCreated', 15 | 'whenChanged', 'whenCreated', 'isDeleted' 16 | ] 17 | 18 | class MSADOU: 19 | def __init__(self): 20 | self.description = None #dunno 21 | self.distinguishedName = None #datetime 22 | self.dSCorePropagationData = None #str 23 | self.gPLink = None #list 24 | self.instanceType = None #int 25 | self.isCriticalSystemObject = None #int 26 | self.name = None #int 27 | self.nTSecurityDescriptor = None #int 28 | self.objectCategory = None #int 29 | self.objectClass = None #str 30 | self.objectGUID = None #int 31 | self.ou = None #int 32 | self.showInAdvancedViewOnly = None #int 33 | self.systemFlags = None #str 34 | self.uSNChanged = None #int 35 | self.uSNCreated = None #str 36 | self.whenChanged = None #str 37 | self.whenCreated = None #str 38 | self.isDeleted = None #str 39 | 40 | @staticmethod 41 | def from_ldap(entry): 42 | adi = MSADOU() 43 | adi.description = entry['attributes'].get('description') #dunno 44 | adi.distinguishedName = entry['attributes'].get('distinguishedName') #datetime 45 | adi.dSCorePropagationData = entry['attributes'].get('dSCorePropagationData') #str 46 | adi.gPLink = entry['attributes'].get('gPLink') #list 47 | adi.instanceType = entry['attributes'].get('instanceType') #int 48 | adi.isCriticalSystemObject = entry['attributes'].get('isCriticalSystemObject') #int 49 | adi.name = entry['attributes'].get('name') #str 50 | adi.nTSecurityDescriptor = entry['attributes'].get('nTSecurityDescriptor') #str 51 | adi.objectCategory = entry['attributes'].get('objectCategory') #str 52 | adi.objectClass = entry['attributes'].get('objectClass') #str 53 | adi.objectGUID = entry['attributes'].get('objectGUID') #str 54 | adi.ou = entry['attributes'].get('ou') #str 55 | adi.showInAdvancedViewOnly = entry['attributes'].get('showInAdvancedViewOnly') #int 56 | adi.systemFlags = entry['attributes'].get('systemFlags') #int 57 | adi.uSNChanged = entry['attributes'].get('uSNChanged') #int 58 | adi.uSNCreated = entry['attributes'].get('uSNCreated') #int 59 | adi.whenChanged = entry['attributes'].get('whenChanged') #datetime 60 | adi.whenCreated = entry['attributes'].get('whenCreated') #datetime 61 | adi.isDeleted = entry['attributes'].get('isDeleted') #datetime 62 | return adi 63 | 64 | def to_dict(self): 65 | d = {} 66 | d['description'] = self.description 67 | d['distinguishedName'] = self.distinguishedName 68 | d['dSCorePropagationData'] = self.dSCorePropagationData 69 | d['gPLink'] = self.gPLink 70 | d['instanceType'] = self.instanceType 71 | d['isCriticalSystemObject'] = self.isCriticalSystemObject 72 | d['name'] = self.name 73 | d['nTSecurityDescriptor'] = self.nTSecurityDescriptor 74 | d['objectCategory'] = self.objectCategory 75 | d['objectClass'] = self.objectClass 76 | d['objectGUID'] = self.objectGUID 77 | d['ou'] = self.ou 78 | d['showInAdvancedViewOnly'] = self.showInAdvancedViewOnly 79 | d['systemFlags'] = self.systemFlags 80 | d['uSNChanged'] = self.uSNChanged 81 | d['uSNCreated'] = self.uSNCreated 82 | d['whenChanged'] = self.whenChanged 83 | d['whenCreated'] = self.whenCreated 84 | d['isDeleted'] = self.isDeleted 85 | return d 86 | 87 | def get_row(self, attrs): 88 | t = self.to_dict() 89 | if 'nTSecurityDescriptor' in attrs: 90 | if t['nTSecurityDescriptor'] is not None: 91 | t['nTSecurityDescriptor'] = base64.b64encode(t['nTSecurityDescriptor']).decode() 92 | else: 93 | t['nTSecurityDescriptor'] = b'' 94 | return [str(t.get(x)) for x in attrs] 95 | 96 | def __str__(self): 97 | t = 'MSADOU\r\n' 98 | d = self.to_dict() 99 | for k in d: 100 | t += '%s: %s\r\n' % (k, d[k]) 101 | return t 102 | 103 | def to_bh(self, domain, domainsid): 104 | return { 105 | 'Aces' : [], 106 | 'Links' : [], 107 | 'ObjectIdentifier' : self.objectGUID.upper(), 108 | "IsDeleted": bool(self.isDeleted), 109 | "IsACLProtected": False , # Post processing 110 | 'Properties' : { 111 | 'name' : '%s@%s' % (self.name.upper(), domain.upper()), 112 | 'domain' : domain, 113 | 'domainsid' : domainsid, 114 | 'distinguishedname' : str(self.distinguishedName).upper(), 115 | 'highvalue' : False, # seems always false 116 | 'whencreated' : bh_dt_convert(self.whenCreated), 117 | 'description' : self.description , 118 | 'blocksinheritance' : False, # seems always false 119 | }, 120 | "GPOChanges": { 121 | "LocalAdmins": [], 122 | "RemoteDesktopUsers": [], 123 | "DcomUsers": [], 124 | "PSRemoteUsers": [], 125 | "AffectedComputers": [] 126 | }, 127 | '_gPLink' : self.gPLink, 128 | } -------------------------------------------------------------------------------- /msldap/ldap_objects/adschemaentry.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | MSAD_SYNTAX_TYPE = { 8 | '2.5.5.1': 'str', 9 | '2.5.5.2': 'str', 10 | '2.5.5.3': 'str', 11 | '2.5.5.4': 'str', 12 | '2.5.5.5': 'str', 13 | '2.5.5.6': 'str', 14 | '2.5.5.7': 'str', 15 | '2.5.5.8': 'bool', 16 | '2.5.5.9': 'int', 17 | '2.5.5.10': 'bytes', 18 | '2.5.5.11': 'date', 19 | '2.5.5.12': 'str', 20 | '2.5.5.13': 'bstr', 21 | '2.5.5.14': 'str', 22 | '2.5.5.15': 'sd', 23 | '2.5.5.16': 'int', 24 | '2.5.5.17': 'sid', 25 | } 26 | 27 | MSADSCHEMAENTRY_ATTRS = [ 28 | 'cn', 'distinguishedName', 'adminDescription', 29 | 'adminDisplayName', 'objectGUID', 'schemaIDGUID', 30 | 'lDAPDisplayName', 'name', 'attributeID', 31 | 'attributeSyntax', 'isSingleValued', 32 | 'isMemberOfPartialAttributeSet' 33 | ] 34 | 35 | class MSADSchemaEntry: 36 | def __init__(self): 37 | self.cn = None #str 38 | self.distinguishedName = None #dn 39 | self.adminDescription = None #dunno 40 | self.adminDisplayName = None #datetime 41 | self.objectGUID = None #int 42 | self.schemaIDGUID = None 43 | self.lDAPDisplayName = None 44 | self.name = None #int 45 | self.attributeID = None 46 | self.attributeSyntax = None 47 | self.isSingleValued = None #str 48 | self.isMemberOfPartialAttributeSet = None 49 | 50 | 51 | @staticmethod 52 | def from_ldap(entry): 53 | adi = MSADSchemaEntry() 54 | adi.cn = entry['attributes'].get('cn') 55 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 56 | adi.adminDescription = entry['attributes'].get('adminDescription') 57 | adi.adminDisplayName = entry['attributes'].get('adminDisplayName') 58 | adi.objectGUID = entry['attributes'].get('objectGUID') #str 59 | adi.schemaIDGUID = entry['attributes'].get('schemaIDGUID') #list 60 | adi.lDAPDisplayName = entry['attributes'].get('lDAPDisplayName') #int 61 | adi.name = entry['attributes'].get('name') #int 62 | adi.attributeID = entry['attributes'].get('attributeID') 63 | adi.attributeSyntax = entry['attributes'].get('attributeSyntax') 64 | adi.isSingleValued = entry['attributes'].get('isSingleValued') 65 | adi.isMemberOfPartialAttributeSet = entry['attributes'].get('isMemberOfPartialAttributeSet') 66 | 67 | return adi 68 | 69 | def to_dict(self): 70 | d = {} 71 | d['cn'] = self.cn 72 | d['distinguishedName'] = self.distinguishedName 73 | d['adminDescription'] = self.adminDescription 74 | d['adminDisplayName'] = self.adminDisplayName 75 | d['objectGUID'] = self.objectGUID 76 | d['schemaIDGUID'] = self.schemaIDGUID 77 | d['lDAPDisplayName'] = self.lDAPDisplayName 78 | d['name'] = self.name 79 | d['attributeID'] = self.attributeID 80 | d['attributeSyntax'] = self.attributeSyntax 81 | d['isSingleValued'] = self.isSingleValued 82 | d['isMemberOfPartialAttributeSet'] = self.isMemberOfPartialAttributeSet 83 | 84 | return d 85 | 86 | def __str__(self): 87 | t = 'MSADSchemaEntry\r\n' 88 | d = self.to_dict() 89 | for k in d: 90 | t += '%s: %s\r\n' % (k, d[k]) 91 | return t 92 | 93 | def get_type(self): 94 | im = 'single' if self.isSingleValued is True else 'multi' 95 | return '%s_%s' % (im, MSAD_SYNTAX_TYPE[self.attributeSyntax]) -------------------------------------------------------------------------------- /msldap/ldap_objects/adsec.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | 8 | from winacl.dtyp.sid import SID 9 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 10 | 11 | class MSADTokenGroup: 12 | def __init__(self): 13 | self.cn = None #str 14 | self.distinguishedName = None #dn 15 | self.objectGUID = None 16 | self.objectSid = None 17 | self.tokengroups = [] 18 | 19 | @staticmethod 20 | def from_ldap(entry): 21 | t = MSADTokenGroup() 22 | t.cn = entry['attributes'].get('cn') 23 | t.distinguishedName = entry['attributes'].get('distinguishedName') 24 | t.objectGUID = entry['attributes'].get('objectGUID') 25 | t.objectSid = entry['attributes'].get('objectSid') 26 | for sid_data in entry['attributes']['tokenGroups']: 27 | t.tokengroups.append(SID.from_bytes(sid_data)) 28 | return t 29 | 30 | def __str__(self): 31 | t = '== MSADTokenGroup ==\r\n' 32 | t+= 'cn : %s\r\n' % self.cn 33 | t+= 'distinguishedName : %s\r\n' % self.distinguishedName 34 | t+= 'objectGUID : %s\r\n' % self.objectGUID 35 | t+= 'objectSid : %s\r\n' % self.objectSid 36 | t+= 'tokengroups : %s\r\n' % [str(x) for x in self.tokengroups] 37 | 38 | return t 39 | 40 | 41 | class MSADSecurityInfo: 42 | ATTRS = [ 'sn', 'cn', 'objectClass','distinguishedName', 'nTSecurityDescriptor', 'objectGUID', 'objectSid'] 43 | 44 | def __init__(self): 45 | self.sn = None #str 46 | self.cn = None #str 47 | self.distinguishedName = None #dn 48 | self.nTSecurityDescriptor = None 49 | self.objectGUID = None 50 | self.objectSid = None 51 | self.objectClass = None 52 | 53 | @staticmethod 54 | def from_ldap(entry): 55 | adi = MSADSecurityInfo() 56 | adi.sn = entry['attributes'].get('sn') 57 | adi.cn = entry['attributes'].get('cn') 58 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 59 | adi.objectGUID = entry['attributes'].get('objectGUID') 60 | adi.objectSid = entry['attributes'].get('objectSid') 61 | adi.objectClass = entry['attributes'].get('objectClass') 62 | adi.nTSecurityDescriptor = entry['attributes'].get('nTSecurityDescriptor') 63 | 64 | return adi 65 | 66 | 67 | def __str__(self): 68 | t = '== MSADSecurityInfo ==\r\n' 69 | t+= 'sn : %s\r\n' % self.sn 70 | t+= 'cn : %s\r\n' % self.cn 71 | t+= 'distinguishedName : %s\r\n' % self.distinguishedName 72 | t+= 'objectGUID : %s\r\n' % self.objectGUID 73 | t+= 'objectSid : %s\r\n' % self.objectSid 74 | t+= 'nTSecurityDescriptor : %s\r\n' % self.nTSecurityDescriptor 75 | 76 | return t -------------------------------------------------------------------------------- /msldap/ldap_objects/adtrust.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import enum 8 | 9 | from winacl.dtyp.sid import SID 10 | from msldap.ldap_objects.common import MSLDAP_UAC, vn 11 | 12 | MSADDomainTrust_ATTRS = [ 13 | 'sn', 14 | 'cn', 15 | 'objectClass', 16 | 'distinguishedName', 17 | 'nTSecurityDescriptor', 18 | 'objectGUID', 19 | 'instanceType', 20 | 'whenCreated', 21 | 'whenChanged', 22 | 'name', 23 | 'securityIdentifier', 24 | 'trustDirection', 25 | 'trustPartner', 26 | 'trustPosixOffset', 27 | 'trustType', 28 | 'trustAttributes', 29 | 'flatName', 30 | 'dSCorePropagationData', 31 | ] 32 | 33 | 34 | class TrustType(enum.Enum): 35 | DOWNLEVEL = 0x00000001 #): The trusted domain is a Windows domain not running Active Directory. 36 | UPLEVEL = 0x00000002 #): The trusted domain is a Windows domain running Active Directory. 37 | MIT = 0x00000003 #): The trusted domain is running a non-Windows, RFC4120-compliant Kerberos distribution. This type of trust is distinguished in that (1) a SID is not required for the TDO, and (2) the default key types include the DES-CBC and DES-CRC encryption types (see [RFC4120] section 8.1). 38 | DCE = 0x00000004 #): Historical reference; this value is not used in Windows. 39 | 40 | # From: https://msdn.microsoft.com/en-us/library/cc223768.aspx 41 | class TrustDirection(enum.Enum): #enum.IntFlag << the actual type is intflag, but noone cares 42 | DISABLED = 0x00000000 #: Absence of any flags. The trust relationship exists but has been disabled. 43 | INBOUND = 0x00000001 #): The trusted domain trusts the primary domain to perform operations such as name lookups and authentication. If this flag is set, then the trustAuthIncoming attribute is present on this object. 44 | OUTBOUND = 0x00000002 #: The primary domain trusts the trusted domain to perform operations such as name lookups and authentication. If this flag is set, then the trustAuthOutgoing attribute is present on this object. 45 | BIDIRECTIONAL = 0x00000003 #: OR'ing of the preceding flags and behaviors representing that both domains trust one another for operations such as name lookups and authentication. 46 | 47 | # https://msdn.microsoft.com/en-us/library/cc223779.aspx 48 | class TrustAttributes(enum.IntFlag): 49 | NON_TRANSITIVE = 0x00000001 #If this bit is set, then the trust cannot be used transitively. For example, if domain A trusts domain B, which in turn trusts domain C, and the A<-->B trust has this attribute set, then a client in domain A cannot authenticate to a server in domain C over the A<-->B<-->C trust linkage 50 | UPLEVEL_ONLY = 0x00000002 #If this bit is set in the attribute, then only Windows 2000 operating system and newer clients can use the trust link. Netlogon does not consume trust objects that have this flag set. 51 | QUARANTINED_DOMAIN = 0x00000004 #If this bit is set, the trusted domain is quarantined and is subject to the rules of SID Filtering as described in [MS-PAC] section 4.1.2.2. 52 | FOREST_TRANSITIVE = 0x00000008 #If this bit is set, the trust link is a cross-forest trust [MS-KILE] between the root domains of two forests, both of which are running in a forest functional level of DS_BEHAVIOR_WIN2003 or greater. Only evaluated on Windows Server 2003 operating system and later. Can only be set if forest and trusted forest are running in a forest functional level of DS_BEHAVIOR_WIN2003 or greater. 53 | CROSS_ORGANIZATION = 0x00000010 # If this bit is set, then the trust is to a domain or forest that is not part of the organization. The behavior controlled by this bit is explained in [MS-KILE] section 3.3.5.7.5 and [MS-APDS] section 3.1.5. Only evaluated on Windows Server 2003 and later. Can only be set if forest and trusted forest are running in a forest functional level of DS_BEHAVIOR_WIN2003 or greater. 54 | WITHIN_FOREST = 0x00000020 #If this bit is set, then the trusted domain is within the same forest. Only evaluated on Windows Server 2003 and later. 55 | TREAT_AS_EXTERNAL = 0x00000040 #If this bit is set, then a cross-forest trust to a domain is to be treated as an external trust for the purposes of SID Filtering. Cross-forest trusts are more stringently filtered than external trusts. This attribute relaxes those cross-forest trusts to be equivalent to external trusts. For more information on how each trust type is filtered, see [MS-PAC] section 4.1.2.2. Only evaluated on Windows Server 2003 and later. Only evaluated if SID Filtering is used. Only evaluated on cross-forest trusts having TRUST_ATTRIBUTE_FOREST_TRANSITIVE. Can only be set if forest and trusted forest are running in a forest functional level of DS_BEHAVIOR_WIN2003 or greater. 56 | USES_RC4_ENCRYPTION = 0x00000080 #This bit is set on trusts with the trustType set to TRUST_TYPE_MIT, which are capable of using RC4 keys. Historically, MIT Kerberos distributions supported only DES and 3DES keys ([RFC4120], [RFC3961]). MIT 1.4.1 adopted the RC4HMAC encryption type common to Windows 2000 [MS-KILE], so trusted domains deploying later versions of the MIT distribution required this bit. For more information, see "Keys and Trusts", section 6.1.6.9.1. Only evaluated on TRUST_TYPE_MIT 57 | CROSS_ORGANIZATION_NO_TGT_DELEGATION = 0x00000200 #If this bit is set, tickets granted under this trust MUST NOT be trusted for delegation. The behavior controlled by this bit is as specified in [MS-KILE] section 3.3.5.7.5. Initially supported on Windows Server 2008 operating system and later. After [MSKB-4490425] is installed, this bit is superseded by the TRUST_ATTRIBUTE_CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION bit. 58 | CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION = 0x00000800 # If this bit is set, tickets granted under this trust MUST be trusted for delegation. The behavior controlled by this bit is as specified in [MS-KILE] section 3.3.5.7.5. Only supported on Windows Server 2008 and later after [MSKB-4490425] updates are installed. 59 | PIM_TRUST = 0x00000400 # If this bit and the TATE bit are set, then a cross-forest trust to a domain is to be treated as Privileged Identity Management trust for the purposes of SID Filtering. For more information on how each trust type is filtered, see [MS-PAC] section 4.1.2.2. Evaluated on Windows Server 2012 R2 operating system only with [MSKB-3155495] installed. Also evaluated on Windows Server 2016 operating system and later. Evaluated only if SID Filtering is used. Evaluated only on cross-forest trusts having TRUST_ATTRIBUTE_FOREST_TRANSITIVE. 60 | 61 | def __str__(self): 62 | if not self.value: 63 | return "NONE" 64 | return '|'.join(m.name for m in self.__class__ if m.value & self.value) 65 | 66 | BH_TRUST_DIR_NAMING = { 67 | TrustDirection.DISABLED: 'Disabled', 68 | TrustDirection.INBOUND: 'Inbound', 69 | TrustDirection.OUTBOUND: 'Outbound', 70 | TrustDirection.BIDIRECTIONAL: 'Bidirectional' 71 | } 72 | 73 | 74 | 75 | class MSADDomainTrust: 76 | def __init__(self): 77 | self.sn = None #str 78 | self.cn = None #str 79 | self.distinguishedName = None #dn 80 | self.objectGUID = None 81 | 82 | self.instanceType = None 83 | self.whenCreated = None 84 | self.whenChanged = None 85 | self.name = None 86 | 87 | self.securityIdentifier = None 88 | self.trustDirection = None 89 | self.trustPartner = None 90 | self.trustPosixOffset = None 91 | self.trustType = None 92 | self.trustAttributes = None 93 | self.flatName = None 94 | self.dSCorePropagationData = None 95 | 96 | 97 | @staticmethod 98 | def from_ldap(entry): 99 | adi = MSADDomainTrust() 100 | adi.sn = entry['attributes'].get('sn') 101 | adi.cn = entry['attributes'].get('cn') 102 | adi.distinguishedName = entry['attributes'].get('distinguishedName') 103 | adi.objectGUID = entry['attributes'].get('objectGUID') 104 | adi.instanceType = entry['attributes'].get('instanceType') 105 | adi.whenCreated = entry['attributes'].get('whenCreated') 106 | adi.whenChanged = entry['attributes'].get('whenChanged') 107 | adi.name = entry['attributes'].get('name') 108 | adi.securityIdentifier = entry['attributes'].get('securityIdentifier') 109 | adi.trustDirection = entry['attributes'].get('trustDirection') 110 | adi.trustPartner = entry['attributes'].get('trustPartner') 111 | adi.trustPosixOffset = entry['attributes'].get('trustPosixOffset') 112 | adi.trustType = entry['attributes'].get('trustType') 113 | adi.trustAttributes = entry['attributes'].get('trustAttributes') 114 | adi.flatName = entry['attributes'].get('flatName') 115 | adi.dSCorePropagationData = entry['attributes'].get('dSCorePropagationData') 116 | 117 | if adi.securityIdentifier is not None: 118 | adi.securityIdentifier = SID.from_bytes(adi.securityIdentifier) 119 | if adi.trustType is not None: 120 | adi.trustType = TrustType(adi.trustType) 121 | if adi.trustDirection is not None: 122 | adi.trustDirection = TrustDirection(adi.trustDirection) 123 | return adi 124 | 125 | def to_dict(self): 126 | return { 127 | 'sn' : self.sn, 128 | 'cn' : self.cn, 129 | 'distinguishedName' : self.distinguishedName, 130 | 'objectGUID' : self.objectGUID, 131 | 'instanceType' : self.instanceType, 132 | 'whenCreated' : self.whenCreated, 133 | 'whenChanged' : self.whenChanged, 134 | 'name' : self.name, 135 | 'securityIdentifier' : self.securityIdentifier, 136 | 'trustDirection' : self.trustDirection, 137 | 'trustPartner' : self.trustPartner, 138 | 'trustPosixOffset' : self.trustPosixOffset, 139 | 'trustType' : self.trustType, 140 | 'trustAttributes' : self.trustAttributes, 141 | 'flatName' : self.flatName, 142 | 'dSCorePropagationData' : self.dSCorePropagationData, 143 | } 144 | 145 | def get_line(self): 146 | return '%s %s %s %s %s' % (self.name, self.trustType, self.trustDirection, TrustAttributes(self.trustAttributes), self.securityIdentifier) 147 | 148 | def __str__(self): 149 | t = '== MSADDomainTrust ==\r\n' 150 | t+= 'sn : %s\r\n' % self.sn 151 | t+= 'cn : %s\r\n' % self.cn 152 | t+= 'distinguishedName : %s\r\n' % self.distinguishedName 153 | t+= 'objectGUID : %s\r\n' % self.objectGUID 154 | t+= 'instanceType : %s\r\n' % self.instanceType 155 | t+= 'whenCreated : %s\r\n' % self.whenCreated 156 | t+= 'whenChanged : %s\r\n' % self.whenChanged 157 | t+= 'name : %s\r\n' % self.name 158 | t+= 'securityIdentifier : %s\r\n' % str(self.securityIdentifier) 159 | t+= 'trustDirection : %s\r\n' % self.trustDirection 160 | t+= 'trustPartner : %s\r\n' % self.trustPartner 161 | t+= 'trustPosixOffset : %s\r\n' % self.trustPosixOffset 162 | t+= 'trustType : %s\r\n' % self.trustType 163 | t+= 'trustAttributes : %s\r\n' % self.trustAttributes 164 | t+= 'flatName : %s\r\n' % self.flatName 165 | t+= 'dSCorePropagationData : %s\r\n' % self.dSCorePropagationData 166 | return t 167 | 168 | def get_row(self, attrs): 169 | t = self.to_dict() 170 | return [str(t.get(x)) for x in attrs] 171 | 172 | def to_bh(self): 173 | tat = TrustAttributes(self.trustAttributes) 174 | if TrustAttributes.WITHIN_FOREST in tat: 175 | is_transitive = True 176 | sid_filtering = TrustAttributes.QUARANTINED_DOMAIN in tat 177 | elif TrustAttributes.FOREST_TRANSITIVE in tat: 178 | is_transitive = True 179 | sid_filtering = True 180 | elif TrustAttributes.CROSS_ORGANIZATION in tat or TrustAttributes.TREAT_AS_EXTERNAL in tat: 181 | is_transitive = False 182 | sid_filtering = True 183 | else: 184 | is_transitive = TrustAttributes.NON_TRANSITIVE not in tat 185 | sid_filtering = True 186 | 187 | dname = self.name 188 | if self.name is None: 189 | dname = '' 190 | dname = dname.upper() 191 | 192 | return { 193 | "TargetDomainName": dname, 194 | "TargetDomainSid": str(self.securityIdentifier), 195 | "IsTransitive": is_transitive, 196 | "TrustDirection": self.trustDirection.value, 197 | "TrustType": self.trustType.value, 198 | "SidFilteringEnabled": sid_filtering 199 | } 200 | 201 | -------------------------------------------------------------------------------- /msldap/ldap_objects/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import enum 8 | from datetime import datetime 9 | 10 | def vn(x): 11 | """ 12 | value or none, returns none if x is an empty list 13 | """ 14 | if x == []: 15 | return None 16 | if isinstance(x, list): 17 | return '|'.join(x) 18 | if isinstance(x, datetime): 19 | return x.isoformat() 20 | return x 21 | 22 | class MSLDAP_UAC(enum.IntFlag): 23 | SCRIPT = 0x00000001 #[ADS_UF_SCRIPT](https://msdn.microsoft.com/library/aa772300) The logon script is executed. 24 | ACCOUNTDISABLE = 0x00000002 #[ADS_UF_ACCOUNTDISABLE](https://msdn.microsoft.com/library/aa772300) The user account is disabled. 25 | HOMEDIR_REQUIRED = 0x00000008 #[ADS_UF_HOMEDIR_REQUIRED](https://msdn.microsoft.com/library/aa772300) The home directory is required. 26 | LOCKOUT = 0x00000010 #[ADS_UF_LOCKOUT](https://msdn.microsoft.com/library/aa772300) The account is currently locked out. 27 | PASSWD_NOTREQD = 0x00000020 #[ADS_UF_PASSWD_NOTREQD](https://msdn.microsoft.com/library/aa772300) No password is required. 28 | PASSWD_CANT_CHANGE = 0x00000040 #[ADS_UF_PASSWD_CANT_CHANGE](https://msdn.microsoft.com/library/aa772300) The user cannot change the password. 29 | ENCRYPTED_TEXT_PASSWORD_ALLOWED = 0x00000080 #[ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED](https://msdn.microsoft.com/library/aa772300) The user can send an encrypted password. 30 | TEMP_DUPLICATE_ACCOUNT = 0x00000100 #[ADS_UF_TEMP_DUPLICATE_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. Also known as a local user account. 31 | NORMAL_ACCOUNT = 0x00000200 #[ADS_UF_NORMAL_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is a default account type that represe nts a typical user. 32 | INTERDOMAIN_TRUST_ACCOUNT = 0x00000800 #[ADS_UF_INTERDOMAIN_TRUST_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is a permit to trust accou nt for a system domain that trusts other do mains. 33 | WORKSTATION_TRUST_ACCOUNT = 0x00001000 #[ADS_UF_WORKSTATION_TRUST_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is a computer account for a computer that is a member of this domain. 34 | SERVER_TRUST_ACCOUNT = 0x00002000 #[ADS_UF_SERVER_TRUST_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is a computer account for a system backup domain controller that is a member of this domain. 35 | NA_1 = 0x00004000 # N/A Not used. 36 | NA_2 = 0x00008000 # N/A Not used. 37 | DONT_EXPIRE_PASSWD = 0x00010000 #[ADS_UF_DONT_EXPIRE_PASSWD](https://msdn.microsoft.com/library/aa772300) The password for this account will never expire. 38 | MNS_LOGON_ACCOUNT = 0x00020000 #[ADS_UF_MNS_LOGON_ACCOUNT](https://msdn.microsoft.com/library/aa772300) This is an MNS logon account. 39 | SMARTCARD_REQUIRED = 0x00040000 #[ADS_UF_SMARTCARD_REQUIRED](https://msdn.microsoft.com/library/aa772300) The user must log on using a smart card. 40 | TRUSTED_FOR_DELEGATION = 0x00080000 #[ADS_UF_TRUSTED_FOR_DELEGATION](https://msdn.microsoft.com/library/aa772300) The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service. 41 | NOT_DELEGATED = 0x00100000 #[ADS_UF_NOT_DELEGATED](https://msdn.microsoft.com/library/aa772300) The security c ontext of the user will not be delegated to a service even if the service account is s et as trusted for Kerberos delegation. 42 | USE_DES_KEY_ONLY = 0x00200000 #[ADS_UF_USE_DES_KEY_ONLY](https://msdn .microsoft.com/library/aa772300) Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys. 43 | DONT_REQUIRE_PREAUTH = 0x00400000 #[ADS_UF_DONT_REQUIRE_PREAUTH](https://msdn.microsoft.com/library/aa772300) This account does not require Kerberos pre-authentication for logon. 44 | PASSWORD_EXPIRED = 0x00800000 #[ADS_UF_PASSWORD_EXPIRED](https://msdn.microsoft.com/library/aa772300) The user password has expired. This flag is created by the system using data from the [ Pwd-L ast-Set](a-pwdlastset.md) attribute and the domain policy. 45 | TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 0x01000000 #[ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION](https://msdn.microsoft.com/library/aa772300) The account is enabled for delegation. This is a security-sensi tive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network. -------------------------------------------------------------------------------- /msldap/network/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/network/__init__.py -------------------------------------------------------------------------------- /msldap/network/packetizer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from msldap import logger 4 | from msldap.protocol.utils import calcualte_length 5 | from asysocks.unicomm.common.packetizers import Packetizer 6 | 7 | class LDAPPacketizer(Packetizer): 8 | def __init__(self): 9 | Packetizer.__init__(self, 65535) 10 | self.in_buffer = b'' 11 | self.is_plain_msg = True 12 | 13 | def process_buffer(self): 14 | preread = 6 15 | remaining_length = -1 16 | while True: 17 | if len(self.in_buffer) < preread: 18 | break 19 | lb = self.in_buffer[:preread] 20 | if self.is_plain_msg is True: 21 | remaining_length = calcualte_length(lb) - preread 22 | else: 23 | remaining_length = int.from_bytes(lb[:4], byteorder = 'big', signed = False) 24 | remaining_length = (remaining_length + 4) - preread 25 | if len(self.in_buffer) >= remaining_length+preread: 26 | data = self.in_buffer[:remaining_length+preread] 27 | self.in_buffer = self.in_buffer[remaining_length+preread:] 28 | yield data 29 | continue 30 | break 31 | 32 | 33 | async def data_out(self, data): 34 | yield data 35 | 36 | async def data_in(self, data): 37 | if data is None: 38 | yield data 39 | self.in_buffer += data 40 | for packet in self.process_buffer(): 41 | yield packet -------------------------------------------------------------------------------- /msldap/protocol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/protocol/__init__.py -------------------------------------------------------------------------------- /msldap/protocol/constants.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | BASE = 0 4 | LEVEL = 1 5 | SUBTREE = 2 6 | 7 | DEREF_NEVER = 0 8 | DEREF_SEARCH = 1 9 | DEREF_BASE = 2 10 | DEREF_ALWAYS = 3 11 | 12 | ALL_ATTRIBUTES = '*' 13 | -------------------------------------------------------------------------------- /msldap/protocol/ldap_filter/__init__.py: -------------------------------------------------------------------------------- 1 | from .filter import Filter 2 | from .parser import ParseError 3 | -------------------------------------------------------------------------------- /msldap/protocol/ldap_filter/soundex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | def soundex(string, scale=4): 5 | string = string.upper() 6 | code = string[0] 7 | string = re.sub(r'[AEIOUYHW]', '', string) 8 | chr_key = {'BFPV': '1', 'CGJKQSXZ': '2', 'DT': '3', 'L': '4', 'MN': '5', 'R': '6'} 9 | 10 | for c in string[1:]: 11 | for k, v in chr_key.items(): 12 | if (c in k) and (v != code[-1]): 13 | code += v 14 | break 15 | 16 | return code.ljust(scale, '0') 17 | 18 | 19 | def soundex_compare(val1, val2): 20 | return soundex(val1) == soundex(val2) 21 | -------------------------------------------------------------------------------- /msldap/protocol/query.py: -------------------------------------------------------------------------------- 1 | from .ldap_filter import Filter as LF 2 | from .ldap_filter.filter import LDAPBase 3 | from asn1crypto.core import ObjectIdentifier 4 | from msldap.protocol.messages import Filter, Filters, \ 5 | AttributeDescription, SubstringFilter, MatchingRuleAssertion, \ 6 | Substrings, Substring 7 | 8 | 9 | def equality(attr, value): 10 | if attr[-1] == ':': 11 | name, oid_raw = attr[:-1].split(':') 12 | return Filter({ 13 | 'extensibleMatch' : MatchingRuleAssertion({ 14 | 'matchingRule' : oid_raw.encode(), 15 | 'type' : name.encode(), 16 | 'matchValue' : rfc4515_encode(value), 17 | 'dnAttributes' : False 18 | }) 19 | }) 20 | 21 | elif value == '*': 22 | return Filter({ 23 | 'present' : AttributeDescription(attr.encode()) 24 | }) 25 | 26 | elif value.startswith('*') and value.endswith('*'): 27 | return Filter({ 28 | 'substrings' : SubstringFilter({ 29 | 'type' : attr.encode(), 30 | 'substrings' : Substrings([ 31 | Substring({ 32 | 'any' : rfc4515_encode(value[1:-1]) 33 | }) 34 | ]) 35 | }) 36 | }) 37 | 38 | elif value.startswith('*') is True: 39 | return Filter({ 40 | 'substrings' : SubstringFilter({ 41 | 'type' : attr.encode(), 42 | 'substrings' : Substrings([ 43 | Substring({ 44 | 'final' : rfc4515_encode(value[1:]) 45 | }) 46 | ]) 47 | }) 48 | }) 49 | 50 | elif value.endswith('*') is True: 51 | return Filter({ 52 | 'substrings' : SubstringFilter({ 53 | 'type' : attr.encode(), 54 | 'substrings' : Substrings([ 55 | Substring({ 56 | 'initial' : rfc4515_encode(value[:-1]) 57 | }) 58 | ]) 59 | }) 60 | }) 61 | 62 | else: 63 | return Filter({ 64 | 'equalityMatch' : { 65 | 'attributeDesc' : attr.encode(), 66 | 'assertionValue' : rfc4515_encode(value) 67 | } 68 | }) 69 | 70 | 71 | def query_syntax_converter_inner(ftr): 72 | if ftr.type == 'filter': 73 | if ftr.comp == '=': 74 | return equality(ftr.attr, ftr.val) 75 | elif ftr.comp == '<=': 76 | key = 'lessOrEqual' 77 | elif ftr.comp == '>=': 78 | key = 'greaterOrEqual' 79 | elif ftr.comp == '~=': 80 | key = 'approxMatch' 81 | 82 | return Filter({ 83 | key : { 84 | 'attributeDesc' : ftr.attr.encode(), 85 | 'assertionValue' : rfc4515_encode(ftr.val) 86 | } 87 | }) 88 | 89 | 90 | elif ftr.type == 'group': 91 | if ftr.comp == '&' or ftr.comp == '|': 92 | if ftr.comp == '&': 93 | key = 'and' 94 | elif ftr.comp == '|': 95 | key = 'or' 96 | 97 | #x = [query_syntax_converter_inner(f) for f in ftr.filters] 98 | return Filter({ 99 | key : Filters([query_syntax_converter_inner(f) for f in ftr.filters]) 100 | }) 101 | elif ftr.comp == '!': 102 | return Filter({ 103 | #'not' : Filter(ftr.filters[0]) 104 | 'not' : query_syntax_converter_inner(ftr.filters[0]) 105 | }) 106 | 107 | def query_syntax_converter(ldap_query_string): 108 | """ 109 | Converts and LDAP query string into a ASN1 Filter object. 110 | warning: the parser has a name collision with the asn1 strucutre! 111 | """ 112 | flt = LF.parse(ldap_query_string) 113 | return query_syntax_converter_inner(flt) 114 | 115 | 116 | def escape_filter_chars(text): 117 | return LDAPBase.escape(text) 118 | 119 | def rfc4515_encode(value): 120 | i = 0 121 | byte_str = b'' 122 | while i < len(value): 123 | if (value[i] == '\\') and i < len(value) - 2: 124 | try: 125 | byte_str += int(value[i + 1: i + 3], 16).to_bytes() 126 | i += 2 127 | except ValueError: # not an ldap escaped value, sends as is 128 | byte_str += b'\\' 129 | else: 130 | byte_str += value[i].encode() 131 | i += 1 132 | return byte_str -------------------------------------------------------------------------------- /msldap/protocol/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def calcualte_length(data): 4 | """ 5 | LDAP protocol doesnt send the total length of the message in the header, 6 | it only sends raw ASN1 encoded data structures, which has the length encoded. 7 | This function "decodes" the length os the asn1 structure, and returns it as int. 8 | """ 9 | if data[1] <= 127: 10 | return data[1] + 2 11 | else: 12 | bcount = data[1] - 128 13 | #if (bcount +2 ) > len(data): 14 | # raise Exception('LDAP data too larage! Length byte count: %s' % bcount) 15 | return int.from_bytes(data[2:2+bcount], byteorder = 'big', signed = False) + bcount + 2 16 | 17 | -------------------------------------------------------------------------------- /msldap/relay/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/relay/__init__.py -------------------------------------------------------------------------------- /msldap/relay/server.py: -------------------------------------------------------------------------------- 1 | from asysocks.unicomm.common.target import UniTarget, UniProto 2 | from asysocks.unicomm.server import UniServer 3 | from msldap.network.packetizer import LDAPPacketizer 4 | from msldap.relay.serverconnection import LDAPRelayServerConnection 5 | from asyauth.protocols.spnego.relay.native import spnegorelay_ntlm_factory 6 | from asyauth.protocols.ntlm.relay.native import NTLMRelaySettings, ntlmrelay_factory 7 | import traceback 8 | import asyncio 9 | 10 | class LDAPServerSettings: 11 | def __init__(self, gssapi_factory): 12 | self.gssapi_factory = gssapi_factory 13 | 14 | @property 15 | def gssapi(self): 16 | return self.gssapi_factory() 17 | 18 | class LDAPRelayServer: 19 | def __init__(self, target, settings): 20 | self.target = target 21 | self.settings = settings 22 | self.server = None 23 | self.serving_task = None 24 | self.connections = {} 25 | self.conn_ctr = 0 26 | 27 | def get_ctr(self): 28 | self.conn_ctr += 1 29 | return self.conn_ctr 30 | 31 | async def __handle_connection(self): 32 | try: 33 | async for connection in self.server.serve(): 34 | print('connection in!') 35 | 36 | smbconnection = LDAPRelayServerConnection(self.settings, connection) 37 | self.connections[self.get_ctr()] = smbconnection 38 | x = asyncio.create_task(smbconnection.run()) 39 | 40 | except Exception as e: 41 | traceback.print_exc() 42 | return 43 | 44 | async def run(self): 45 | self.server = UniServer(self.target, LDAPPacketizer()) 46 | self.serving_task = asyncio.create_task(self.__handle_connection()) 47 | return self.serving_task 48 | 49 | async def test_relay_queue(rq): 50 | try: 51 | from aiosmb.connection import SMBConnection 52 | from aiosmb.commons.connection.target import SMBTarget 53 | from aiosmb.commons.interfaces.machine import SMBMachine 54 | test_target = SMBTarget('10.10.10.2') 55 | while True: 56 | item = await rq.get() 57 | print(item) 58 | connection = SMBConnection(item, test_target, preserve_gssapi=False, nosign=True) 59 | _, err = await connection.login() 60 | if err is not None: 61 | print('SMB client login err: %s' % err) 62 | print(traceback.format_tb(err.__traceback__)) 63 | continue 64 | machine = SMBMachine(connection) 65 | async for share, err in machine.list_shares(): 66 | if err is not None: 67 | print('SMB client list_shares err: %s' % err) 68 | continue 69 | print(share) 70 | 71 | except Exception as e: 72 | traceback.print_exc() 73 | return 74 | 75 | async def amain(): 76 | try: 77 | auth_relay_queue = asyncio.Queue() 78 | x = asyncio.create_task(test_relay_queue(auth_relay_queue)) 79 | target = UniTarget('0.0.0.0', 636, UniProto.SERVER_SSL_TCP) 80 | 81 | settings = LDAPServerSettings(lambda: spnegorelay_ntlm_factory(auth_relay_queue, lambda: ntlmrelay_factory())) 82 | server = LDAPRelayServer(target, settings) 83 | server_task = await server.run() 84 | await server_task 85 | except Exception as e: 86 | traceback.print_exc() 87 | return 88 | if __name__ == '__main__': 89 | asyncio.run(amain()) -------------------------------------------------------------------------------- /msldap/relay/serverconnection.py: -------------------------------------------------------------------------------- 1 | from msldap.protocol.utils import calcualte_length 2 | from msldap.protocol.messages import LDAPMessage, AuthenticationChoice, protocolOp, BindResponse 3 | from msldap import logger 4 | import traceback 5 | import asyncio 6 | 7 | class LDAPRelayServerConnection: 8 | def __init__(self, settings, connection): 9 | self.settings = settings 10 | self.gssapi = settings.gssapi 11 | self.ntlm = self.gssapi.authentication_contexts['NTLMSSP - Microsoft NTLM Security Support Provider'] 12 | self.connection = connection 13 | self.__auth_type = None 14 | self.__auth_iter = 0 15 | 16 | async def log_async(self, level, msg): 17 | if self.settings.log_q is not None: 18 | src = 'LDAPCON-%s:%s' % (self.client_ip, self.client_port) 19 | await self.settings.log_q.put((src, level, msg)) 20 | else: 21 | logger.log(level, msg) 22 | 23 | async def terminate(self): 24 | self.handle_in_task.cancel() 25 | 26 | async def __handle_ldap_in(self): 27 | async for msg_data in self.connection.read(): 28 | try: 29 | msg = LDAPMessage.load(msg_data) 30 | if msg['protocolOp']._choice == 0: 31 | await self.__bindreq(msg) 32 | else: 33 | raise Exception('Unknown LDAP message! %s' % msg.native) 34 | 35 | except Exception as e: 36 | await self.log_async(1, str(e)) 37 | return 38 | 39 | async def __bindreq(self, msg): 40 | try: 41 | msg_id = msg.native['messageID'] 42 | authdata_raw = msg.native['protocolOp']['authentication'] 43 | if isinstance(authdata_raw, bytes) is True: 44 | self.__auth_type = 'NTLM' 45 | if self.__auth_iter == 0: 46 | t = { 47 | 'resultCode' : 0, 48 | 'matchedDN' : 'NTLM'.encode(), 49 | 'diagnosticMessage' : b'', 50 | } 51 | po = {'bindResponse' : BindResponse(t)} 52 | b= { 53 | 'messageID' : msg_id, 54 | 'protocolOp' : protocolOp(po), 55 | } 56 | resp = LDAPMessage(b) 57 | await self.connection.write(resp.dump()) 58 | self.__auth_iter += 1 59 | return 60 | 61 | else: 62 | resdata, to_conitnue, err = await self.ntlm.authenticate_relay_server(authdata_raw) 63 | if err is not None: 64 | raise err 65 | 66 | if resdata is None: 67 | t = { 68 | 'resultCode' : 49, 69 | 'matchedDN' : b'', 70 | 'diagnosticMessage' : b'8009030C: LdapErr: DSID-0C090569, comment: AcceptSecurityContext error, data 52e, v4563\x00', 71 | } 72 | po = {'bindResponse' : BindResponse(t)} 73 | b= { 74 | 'messageID' : msg_id, 75 | 'protocolOp' : protocolOp(po), 76 | } 77 | resp = LDAPMessage(b) 78 | 79 | await self.connection.write(resp.dump()) 80 | 81 | await self.terminate() 82 | return 83 | 84 | t = { 85 | 'resultCode' : 0, 86 | 'matchedDN' : resdata, 87 | 'diagnosticMessage' : b'', 88 | } 89 | po = {'bindResponse' : BindResponse(t)} 90 | b= { 91 | 'messageID' : msg_id, 92 | 'protocolOp' : protocolOp(po), 93 | } 94 | resp = LDAPMessage(b) 95 | await self.connection.write(resp.dump()) 96 | self.__auth_iter += 1 97 | return 98 | 99 | if isinstance(authdata_raw, dict) is True: 100 | self.__auth_type = 'GSSAPI' 101 | resdata, to_conitnue, err = await self.gssapi.authenticate_relay_server(authdata_raw['credentials']) 102 | if err is not None: 103 | raise err 104 | 105 | if resdata is None: 106 | t = { 107 | 'resultCode' : 49, 108 | 'matchedDN' : b'', 109 | 'diagnosticMessage' : b'8009030C: LdapErr: DSID-0C090569, comment: AcceptSecurityContext error, data 52e, v4563\x00', 110 | } 111 | po = {'bindResponse' : BindResponse(t)} 112 | b= { 113 | 'messageID' : msg_id, 114 | 'protocolOp' : protocolOp(po), 115 | } 116 | resp = LDAPMessage(b) 117 | 118 | await self.connection.write(resp.dump()) 119 | 120 | await self.terminate() 121 | return 122 | 123 | t = { 124 | 'resultCode' : 14, 125 | 'matchedDN' : b'', 126 | 'diagnosticMessage' : b'', 127 | 'serverSaslCreds': resdata 128 | } 129 | po = {'bindResponse' : BindResponse(t)} 130 | b= { 131 | 'messageID' : msg_id, 132 | 'protocolOp' : protocolOp(po), 133 | } 134 | resp = LDAPMessage(b) 135 | await self.connection.write(resp.dump()) 136 | self.__auth_iter += 1 137 | 138 | else: 139 | raise Exception('Unknown auth method %s' % authdata_raw) 140 | 141 | 142 | except Exception as e: 143 | traceback.print_exc() 144 | return 145 | 146 | async def run(self): 147 | self.handle_in_task = asyncio.create_task(self.__handle_ldap_in()) 148 | await self.handle_in_task -------------------------------------------------------------------------------- /msldap/wintypes/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | from .asn1.sdflagsrequest import SDFlagsRequest, SDFlagsRequestValue 8 | 9 | 10 | __all__ = ['SDFlagsRequest', 'SDFlagsRequestValue'] 11 | -------------------------------------------------------------------------------- /msldap/wintypes/asn1/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | 8 | -------------------------------------------------------------------------------- /msldap/wintypes/asn1/sdflagsrequest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Author: 4 | # Tamas Jos (@skelsec) 5 | # 6 | 7 | import enum 8 | from asn1crypto import core 9 | 10 | class SDFlagsRequest(enum.IntFlag): 11 | OWNER_SECURITY_INFORMATION = 0x1 #Owner identifier of the object.(OSI) 12 | GROUP_SECURITY_INFORMATION = 0x2 #Primary group identifier.(GSI) 13 | DACL_SECURITY_INFORMATION = 0x4 #Discretionary access control list (DACL) of the object.(DSI) 14 | SACL_SECURITY_INFORMATION = 0x8 #System access control list (SACL) of the object.(SSI) 15 | 16 | # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/3888c2b7-35b9-45b7-afeb-b772aa932dd0 17 | class SDFlagsRequestValue(core.Sequence): 18 | _fields = [ 19 | ('Flags', core.Integer), 20 | ] -------------------------------------------------------------------------------- /msldap/wintypes/dnsp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skelsec/msldap/84cbdad5ea190b4fbc263883cafb65f63742563f/msldap/wintypes/dnsp/__init__.py -------------------------------------------------------------------------------- /msldap/wintypes/dnsp/structures/__init__.py: -------------------------------------------------------------------------------- 1 | from .dnsproperty import * 2 | from .dnsrecord import * 3 | from .misc import * 4 | -------------------------------------------------------------------------------- /msldap/wintypes/dnsp/structures/dnsproperty.py: -------------------------------------------------------------------------------- 1 | # NOTE: implementation is based on https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/445c7843-e4a1-4222-8c0f-630c230a4c80 2 | 3 | from __future__ import annotations 4 | 5 | import sys 6 | from typing import ClassVar, Generic, List, Optional, TypeVar 7 | 8 | from msldap.commons.utils import timestamp2datetime 9 | from msldap.wintypes.dnsp.structures.misc import DnsAddrArray, Ip4Array 10 | 11 | # NOTE: typing.Type is deprecated since version 3.9 12 | if sys.version_info < (3, 9): 13 | from typing import Type 14 | else: 15 | Type = type 16 | 17 | 18 | # NOTE: to make typing happy with auto-implementation of from_bytes 19 | class _Unserializable: 20 | @classmethod 21 | def from_bytes(cls, data: bytes, byteorder: str) -> _Unserializable: 22 | ... 23 | 24 | 25 | # NOTE: by default, consider BaseType unserializable to make possible the generation of from_bytes method 26 | BaseType = TypeVar("BaseType", bound=_Unserializable) 27 | 28 | 29 | class BaseDnsProperty(Generic[BaseType]): 30 | _id: ClassVar[int] = None 31 | _name: ClassVar[str] = None 32 | 33 | _base_type: ClassVar[Type[BaseType]] = None 34 | _default: ClassVar[BaseType] = None 35 | 36 | _implementations: ClassVar[List[Type[BaseDnsProperty]]] = [] 37 | 38 | def __init__(self, value: Optional[BaseType] = None) -> None: 39 | self._value = value if value is not None else self._default 40 | 41 | def __str__(self) -> str: 42 | return f"{self._name}: {self.value}" 43 | 44 | @property 45 | def value(self) -> str: 46 | return str(self._value) 47 | 48 | @classmethod 49 | def from_bytes(cls, data: bytes) -> BaseDnsProperty: 50 | return cls(cls._base_type.from_bytes(data, "little")) 51 | 52 | def __init_subclass__(cls, **kwargs) -> None: 53 | super().__init_subclass__(**kwargs) 54 | 55 | cls._base_type = cls.__orig_bases__[0].__args__[0] 56 | 57 | if (cls._id is not None) and (cls._name is not None): 58 | cls._implementations.append(cls) 59 | 60 | 61 | class DnsEnumProperty(BaseDnsProperty[int]): 62 | _types: ClassVar[dict[int, str]] = None 63 | 64 | @property 65 | def value(self) -> str: 66 | return self._types.get(self._value, "Unknown") 67 | 68 | 69 | class DnsFlagsProperty(BaseDnsProperty[int]): 70 | _flags: ClassVar[dict[int, str]] = None 71 | 72 | @property 73 | def value(self) -> str: 74 | return " | ".join( 75 | value for (key, value) in self._flags.items() if (self._value & key) 76 | ) 77 | 78 | 79 | class DnsHoursIntervalProperty(BaseDnsProperty[int]): 80 | @property 81 | def value(self) -> str: 82 | if self._value == 0: 83 | return "0 hours" 84 | 85 | result = "" 86 | 87 | days = self._value // 24 88 | if days != 0: 89 | result += f"{days} days " 90 | 91 | hours = self._value % 24 92 | if hours != 0: 93 | result += f"{hours} hours " 94 | 95 | return result.rstrip() 96 | 97 | 98 | class DnsTimestampProperty(BaseDnsProperty[int]): 99 | @property 100 | def value(self) -> str: 101 | if self._value == 0: 102 | return "Never" 103 | 104 | return timestamp2datetime(self._value).isoformat() 105 | 106 | 107 | class DnsNullTerminatedStringProperty(BaseDnsProperty[str]): 108 | @classmethod 109 | def from_bytes(cls, data: bytes) -> DnsNullTerminatedStringProperty: 110 | return cls(data.decode("utf-8").rstrip("\x00")) 111 | 112 | 113 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/27e138a7-110c-44a4-afcb-b95f35f00306 114 | class DnsZoneType(DnsEnumProperty): 115 | _id = 0x01 116 | _name = "Zone Type" 117 | 118 | _default = 1 119 | 120 | _types = { 121 | 0: "Cache", 122 | 1: "Primary", 123 | 2: "Secondary", 124 | 3: "Stub", 125 | 4: "Forwarder", 126 | 5: "Secondary Cache", 127 | } 128 | 129 | 130 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/d4b84209-f00c-478f-80d7-8dd0f1633d9e 131 | class DnsZoneAllowUpdate(DnsEnumProperty): 132 | _id = 0x02 133 | _name = "Dynamic Updates" 134 | 135 | _default = -1 136 | 137 | _types = { 138 | 0: "Not allowed", 139 | 1: "All updates allowed", 140 | 2: "Only secure updates allowed", 141 | } 142 | 143 | 144 | class DnsZoneSecureTime(DnsTimestampProperty): 145 | _id = 0x08 146 | _name = "Time Zone Secured" 147 | 148 | _default = 0 149 | 150 | 151 | class DnsZoneNoRefreshInterval(DnsHoursIntervalProperty): 152 | _id = 0x10 153 | _name = "Zone No-Refresh Interval" 154 | 155 | _default = 168 156 | 157 | 158 | class DnsZoneRefreshInterval(DnsHoursIntervalProperty): 159 | _id = 0x20 160 | _name = "Zone Refresh Interval" 161 | 162 | _default = 168 163 | 164 | 165 | class DnsZoneAgingState(BaseDnsProperty[bool]): 166 | _id = 0x40 167 | _name = "Aging Enabled" 168 | 169 | _default = False 170 | 171 | 172 | class DnsZoneScavengingServers(BaseDnsProperty[Ip4Array]): 173 | _id = 0x11 174 | _name = "DNS Servers performing Scavenging" 175 | 176 | _default = Ip4Array() 177 | 178 | 179 | class DnsZoneAgingEnabledTime(DnsHoursIntervalProperty): 180 | _id = 0x12 181 | _name = "Time before the next Scavenging Cycle" 182 | 183 | _default = 0 184 | 185 | 186 | class DnsZoneDeletedFromHostname(DnsNullTerminatedStringProperty): 187 | _id = 0x80 188 | _name = "Name of Server that deleted the Zone" 189 | 190 | _default = "Unknown" 191 | 192 | 193 | class DnsZoneMasterServers(BaseDnsProperty[Ip4Array]): 194 | _id = 0x81 195 | _name = "DNS Servers performing Zone Transfers" 196 | 197 | _default = Ip4Array() 198 | 199 | 200 | class DnsZoneAutoNsServers(BaseDnsProperty[Ip4Array]): 201 | _id = 0x82 202 | _name = "DNS Servers which may autocreate a Delegation" 203 | 204 | _default = Ip4Array() 205 | 206 | 207 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/4ec7bdf7-1807-4179-96af-ce1c1cd448b7 208 | class DnsZoneDcPromoConvert(DnsEnumProperty): 209 | _id = 0x83 210 | _name = "State of Conversion" 211 | 212 | _default = 0 213 | 214 | _types = { 215 | 0: "None", 216 | 1: "To be moved to DNS Domain partition", 217 | 2: "To be moved to DNS Forest partition", 218 | } 219 | 220 | 221 | class DnsZoneScavengingServersDa(BaseDnsProperty[DnsAddrArray]): 222 | _id = 0x90 223 | _name = "DNS Servers performing Scavenging" 224 | 225 | _default = DnsAddrArray() 226 | 227 | 228 | class DnsZoneMasterServersDa(BaseDnsProperty[DnsAddrArray]): 229 | _id = 0x91 230 | _name = "DNS Servers performing Zone Transfers" 231 | 232 | _default = DnsAddrArray() 233 | 234 | 235 | class DnsZoneAutoNsServersDa(BaseDnsProperty[DnsAddrArray]): 236 | _id = 0x92 237 | _name = "DNS Servers which may autocreate a Delegation" 238 | 239 | _default = DnsAddrArray() 240 | 241 | 242 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/f448341f-512d-414a-aaa3-e303d592fcd2 243 | class DnsZoneNodeDbFlags(DnsFlagsProperty): 244 | _id = 0x100 245 | _name = "DB Flags" 246 | 247 | _default = 0 248 | 249 | _flags = { 250 | 0x80000000: "DNS_RPC_FLAG_CACHE_DATA", 251 | 0x40000000: "DNS_RPC_FLAG_ZONE_ROOT", 252 | 0x20000000: "DNS_RPC_FLAG_AUTH_ZONE_ROOT", 253 | 0x10000000: "DNS_RPC_FLAG_ZONE_DELEGATION", 254 | 0x08000000: "DNS_RPC_FLAG_RECORD_DEFAULT_TTL", 255 | 0x04000000: "DNS_RPC_FLAG_RECORD_TTL_CHANGE", 256 | 0x02000000: "DNS_RPC_FLAG_RECORD_CREATE_PTR", 257 | 0x01000000: "DNS_RPC_FLAG_NODE_STICKY", 258 | 0x00800000: "DNS_RPC_FLAG_NODE_COMPLETE", 259 | 0x00010000: "DNS_RPC_FLAG_SUPPRESS_NOTIFY", 260 | 0x00020000: "DNS_RPC_FLAG_AGING_ON", 261 | 0x00040000: "DNS_RPC_FLAG_OPEN_ACL", 262 | 0x00100000: "DNS_RPC_FLAG_RECORD_WIRE_FORMAT", 263 | 0x00200000: "DNS_RPC_FLAG_SUPPRESS_RECORD_UPDATE_PTR", 264 | } 265 | 266 | 267 | class DnsPropertyFactory: 268 | def __init__(self) -> None: 269 | self._factories = { 270 | subclass._id: subclass for subclass in BaseDnsProperty._implementations 271 | } 272 | 273 | def from_bytes(self, data: bytes) -> Optional[BaseDnsProperty]: 274 | length = int.from_bytes(data[:4], "little") 275 | id = int.from_bytes(data[16:20], "little") 276 | 277 | FactoryClass = self._factories.get(id) 278 | if FactoryClass is None: 279 | return None 280 | 281 | if length == 0: 282 | return FactoryClass() 283 | 284 | return FactoryClass.from_bytes(data[20 : 20 + length]) 285 | -------------------------------------------------------------------------------- /msldap/wintypes/dnsp/structures/misc.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import List, Optional 4 | 5 | from msldap.commons.utils import bytes2ipv4, bytes2ipv6 6 | 7 | 8 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/588ae296-71bf-402f-9996-86ecee39dc29 9 | class Ip4Array: 10 | def __init__(self, servers: List[str] = []) -> Ip4Array: 11 | self._servers = servers.copy() 12 | 13 | @classmethod 14 | def from_bytes(cls, data: bytes, _: Optional[str] = None) -> Ip4Array: 15 | num_ips = int.from_bytes(data[:4], "little") 16 | 17 | ips = [bytes2ipv4(data[i * 4 + 4 : i * 4 + 8]) for i in range(num_ips)] 18 | 19 | return Ip4Array(ips) 20 | 21 | def __str__(self) -> str: 22 | if self._servers == []: 23 | return "None" 24 | 25 | return ", ".join(self._servers) 26 | 27 | 28 | # NOTE: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dnsp/56ba5fab-f304-4866-99a4-4f1c1f9247a3 29 | class DnsAddrArray: 30 | AF_IPV4 = 0x0002 31 | AF_IPV6 = 0x0017 32 | 33 | def __init__(self, servers: List[str] = []) -> DnsAddrArray: 34 | self._servers = servers.copy() 35 | 36 | @classmethod 37 | def from_bytes(cls, data: bytes, _: Optional[str] = None) -> DnsAddrArray: 38 | num_addrs = int.from_bytes(data[:4], "little") 39 | 40 | servers = list() 41 | 42 | for i in range(num_addrs): 43 | raw_addr = data[32 + i * 64 : 32 + (i + 1) * 64] 44 | 45 | family = int.from_bytes(raw_addr[:2], "little") 46 | if family == cls.AF_IPV4: 47 | ip = bytes2ipv4(raw_addr[4:8]) 48 | elif family == cls.AF_IPV6: 49 | ip = bytes2ipv6(raw_addr[8:24]) 50 | else: 51 | ip = "Unknown" 52 | 53 | port = int.from_bytes(raw_addr[2:4], "big") 54 | 55 | servers.append(f"{ip}:{port}") 56 | 57 | return DnsAddrArray(servers) 58 | 59 | def __str__(self) -> str: 60 | if self._servers == []: 61 | return "None" 62 | 63 | return ", ".join(self._servers) 64 | -------------------------------------------------------------------------------- /msldap/wintypes/encryptedlaps.py: -------------------------------------------------------------------------------- 1 | import io 2 | import enum 3 | from asn1crypto.cms import ContentInfo 4 | 5 | class EncryptedLAPSBlob: 6 | def __init__(self): 7 | self.update_timestamp: int 8 | self.flags: int 9 | self.blob: bytes 10 | self.asn1blob: ContentInfo 11 | 12 | def get_keyidentifier(self): 13 | sid = self.asn1blob.native['content']['recipient_infos'][0]['kekid']['other']['key_attr']['1']['0']['0']['1'] 14 | print(sid) 15 | return LAPS_KEYIDENTIFIER.from_bytes(self.asn1blob.native['content']['recipient_infos'][0]['kekid']['key_identifier']) 16 | 17 | def from_bytes(data: bytes): 18 | return EncryptedLAPSBlob.from_buffer(io.BytesIO(data)) 19 | 20 | def from_buffer(buff: io.BytesIO): 21 | blob = EncryptedLAPSBlob() 22 | blob.update_timestamp = int.from_bytes(buff.read(8), byteorder='little', signed=False) 23 | blob_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) 24 | blob.flags = int.from_bytes(buff.read(4), byteorder='little', signed=False) 25 | blob.blob = buff.read(blob_length) 26 | blob.asn1blob = ContentInfo.load(blob.blob) 27 | return blob 28 | 29 | def __str__(self): 30 | t = '' 31 | for k in self.__dict__: 32 | t += '%s: %s\n' % (k, self.__dict__[k]) 33 | return t 34 | 35 | class KEYIDFLAGS(enum.IntFlag): 36 | DHPARAMS = 1 37 | UNKNOWN = 2 38 | 39 | 40 | class LAPS_KEYIDENTIFIER: 41 | def __init__(self): 42 | self.version = None 43 | self.magic = None 44 | self.flags = None 45 | self.l0_index = None 46 | self.l1_index = None 47 | self.l2_index = None 48 | self.root_key_identifier = None 49 | self.unknown_length = None 50 | self.domain_length = None 51 | self.forest_length = None 52 | self.unknown = None 53 | self.domain = None 54 | self.forest = None 55 | 56 | @staticmethod 57 | def from_bytes(data:bytes): 58 | return LAPS_KEYIDENTIFIER.from_buffer(io.BytesIO(data)) 59 | 60 | @staticmethod 61 | def from_buffer(buff:io.BytesIO): 62 | blob = LAPS_KEYIDENTIFIER() 63 | blob.version = int.from_bytes(buff.read(4), byteorder='little', signed=False) 64 | blob.magic = buff.read(4) 65 | blob.flags = KEYIDFLAGS(int.from_bytes(buff.read(4), byteorder='little', signed=False)) 66 | blob.l0_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) 67 | blob.l1_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) 68 | blob.l2_index = int.from_bytes(buff.read(4), byteorder='little', signed=False) 69 | blob.root_key_identifier = buff.read(16) 70 | blob.unknown_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) 71 | blob.domain_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) 72 | blob.forest_length = int.from_bytes(buff.read(4), byteorder='little', signed=False) 73 | blob.unknown = buff.read(blob.unknown_length) 74 | blob.domain = buff.read(blob.domain_length) 75 | blob.forest = buff.read(blob.forest_length) 76 | return blob 77 | 78 | def __str__(self): 79 | t = '' 80 | for k in self.__dict__: 81 | if isinstance(self.__dict__[k], bytes): 82 | t += '%s: %s\n' % (k, self.__dict__[k].hex()) 83 | else: 84 | t += '%s: %s\n' % (k, self.__dict__[k]) 85 | return t -------------------------------------------------------------------------------- /msldap/wintypes/managedpassword.py: -------------------------------------------------------------------------------- 1 | import io 2 | from unicrypto.hashlib import md4 3 | 4 | class MSDS_MANAGEDPASSWORD_BLOB: 5 | def __init__(self): 6 | self.Version = None 7 | self.Reserved = None 8 | self.Length = None 9 | self.CurrentPasswordOffset = None 10 | self.PreviousPasswordOffset = None 11 | self.QueryPasswordIntervalOffset = None 12 | self.UnchangedPasswordIntervalOffset = None 13 | self.CurrentPassword = None 14 | self.PreviousPassword = None 15 | #('AlignmentPadding',':'), 16 | self.QueryPasswordInterval = None 17 | self.UnchangedPasswordInterval = None 18 | 19 | self.nt_hash = None 20 | 21 | @staticmethod 22 | def from_bytes(data:bytes): 23 | return MSDS_MANAGEDPASSWORD_BLOB.from_buffer(io.BytesIO(data)) 24 | 25 | @staticmethod 26 | def from_buffer(buff:io.BytesIO): 27 | blob = MSDS_MANAGEDPASSWORD_BLOB() 28 | blob.Version = int.from_bytes(buff.read(2), byteorder='little', signed=False) 29 | blob.Reserved = int.from_bytes(buff.read(2), byteorder='little', signed=False) 30 | blob.Length = int.from_bytes(buff.read(4), byteorder='little', signed=False) 31 | blob.CurrentPasswordOffset = int.from_bytes(buff.read(2), byteorder='little', signed=False) 32 | blob.PreviousPasswordOffset = int.from_bytes(buff.read(2), byteorder='little', signed=False) 33 | blob.QueryPasswordIntervalOffset = int.from_bytes(buff.read(2), byteorder='little', signed=False) 34 | blob.UnchangedPasswordIntervalOffset = int.from_bytes(buff.read(2), byteorder='little', signed=False) 35 | 36 | ppwo = blob.PreviousPasswordOffset 37 | if ppwo == 0: 38 | ppwo = blob.QueryPasswordIntervalOffset 39 | if blob.CurrentPasswordOffset >0: 40 | buff.seek(blob.CurrentPasswordOffset, 0) 41 | blob.CurrentPassword = buff.read(ppwo - blob.CurrentPasswordOffset) 42 | if blob.PreviousPasswordOffset >0: 43 | buff.seek(blob.PreviousPasswordOffset, 0) 44 | blob.PreviousPassword = buff.read(blob.QueryPasswordIntervalOffset - blob.CurrentPasswordOffset) 45 | if blob.QueryPasswordIntervalOffset >0: 46 | buff.seek(blob.QueryPasswordIntervalOffset, 0) 47 | blob.QueryPasswordInterval = buff.read(blob.UnchangedPasswordIntervalOffset - blob.UnchangedPasswordIntervalOffset) 48 | if blob.UnchangedPasswordIntervalOffset >0: 49 | buff.seek(blob.UnchangedPasswordIntervalOffset, 0) 50 | blob.UnchangedPasswordInterval = buff.read() 51 | 52 | 53 | blob.nt_hash = md4(blob.CurrentPassword[:-2]).hexdigest() 54 | return blob 55 | 56 | def __str__(self): 57 | t = '' 58 | for k in self.__dict__: 59 | t += '%s: %s\r\n' % (k, self.__dict__[k]) 60 | return t -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0"] 3 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import re 3 | 4 | VERSIONFILE="msldap/_version.py" 5 | verstrline = open(VERSIONFILE, "rt").read() 6 | VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" 7 | mo = re.search(VSRE, verstrline, re.M) 8 | if mo: 9 | verstr = mo.group(1) 10 | else: 11 | raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) 12 | 13 | 14 | setup( 15 | # Application name: 16 | name="msldap", 17 | 18 | # Version number (initial): 19 | version=verstr, 20 | 21 | # Application author details: 22 | author="Tamas Jos", 23 | author_email="info@skelsecprojects.com", 24 | 25 | # Packages 26 | packages=find_packages(exclude=["tests*"]), 27 | 28 | # Include additional files into the package 29 | include_package_data=True, 30 | 31 | 32 | # Details 33 | url="https://github.com/skelsec/msldap", 34 | 35 | zip_safe = False, 36 | # 37 | # license="LICENSE.txt", 38 | description="Python library to play with MS LDAP", 39 | long_description="Python library to play with MS LDAP", 40 | 41 | # long_description=open("README.txt").read(), 42 | python_requires='>=3.7', 43 | classifiers=[ 44 | "Programming Language :: Python :: 3.7", 45 | "Programming Language :: Python :: 3.8", 46 | "License :: OSI Approved :: MIT License", 47 | "Operating System :: OS Independent", 48 | ], 49 | install_requires=[ 50 | 'unicrypto>=0.0.10', 51 | 'asyauth>=0.0.18', 52 | 'asysocks>=0.2.11', 53 | 'asn1crypto>=1.3.0', 54 | 'winacl>=0.1.8', 55 | 'prompt-toolkit>=3.0.2', 56 | 'tqdm', 57 | 'wcwidth', 58 | 'tabulate', 59 | ], 60 | entry_points={ 61 | 'console_scripts': [ 62 | 'msldap = msldap.examples.msldapclient:main', 63 | 'msldap-bloodhound = msldap.examples.msldapbloodhound:main', 64 | ], 65 | } 66 | ) 67 | --------------------------------------------------------------------------------