├── .assets
└── usage.png
├── .github
└── workflows
│ ├── pytest.yml
│ └── require-changelog-updates.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bofhound
├── __init__.py
├── __main__.py
├── ad
│ ├── __init__.py
│ ├── adds.py
│ ├── helpers
│ │ ├── __init__.py
│ │ ├── cert_utils.py
│ │ ├── propertieslevel.py
│ │ ├── trustdirection.py
│ │ └── trusttype.py
│ └── models
│ │ ├── __init__.py
│ │ ├── bloodhound_aiaca.py
│ │ ├── bloodhound_certtemplate.py
│ │ ├── bloodhound_computer.py
│ │ ├── bloodhound_container.py
│ │ ├── bloodhound_crossref.py
│ │ ├── bloodhound_domain.py
│ │ ├── bloodhound_domaintrust.py
│ │ ├── bloodhound_enterpriseca.py
│ │ ├── bloodhound_gpo.py
│ │ ├── bloodhound_group.py
│ │ ├── bloodhound_issuancepolicy.py
│ │ ├── bloodhound_ntauthstore.py
│ │ ├── bloodhound_object.py
│ │ ├── bloodhound_ou.py
│ │ ├── bloodhound_rootca.py
│ │ ├── bloodhound_schema.py
│ │ └── bloodhound_user.py
├── local
│ ├── __init__.py
│ ├── localbroker.py
│ └── models
│ │ ├── __init__.py
│ │ ├── local_groupmembership.py
│ │ ├── local_privilegedsession.py
│ │ ├── local_registrysession.py
│ │ └── local_session.py
├── logger.py
├── parsers
│ ├── __init__.py
│ ├── brc4_ldap_sentinel.py
│ ├── generic_parser.py
│ ├── havoc.py
│ ├── ldap_search_bof.py
│ ├── outflankc2.py
│ ├── parsertype.py
│ └── shared_parsers
│ │ ├── __init__.py
│ │ ├── netlocalgroup_bof.py
│ │ ├── netloggedon_bof.py
│ │ ├── netsession_bof.py
│ │ ├── regsession_bof.py
│ │ └── sharedparser.py
└── writer.py
├── poetry.lock
├── pyproject.toml
└── tests
├── __init__.py
├── ad
├── __init__.py
├── models
│ ├── __init__.py
│ ├── test_bloodhound_computer.py
│ ├── test_bloodhound_crossref.py
│ ├── test_bloodhound_object.py
│ └── test_bloodhound_user.py
└── test_adds.py
├── local
└── test_local_broker.py
├── parsers
├── __init__.py
├── test_brc4_ldap_sentinel.py
├── test_generic_parser.py
└── test_ldap_search_bof.py
├── test_data
├── __init__.py
├── brc4_ldap_sentinel_logs
│ └── badger_no_acl_1030_objects.log
├── havoc_logs
│ └── Console_73169420.log
├── json_output
│ ├── computers_20220413_204956.json
│ ├── domains_20220413_204956.json
│ ├── groups_20220413_204957.json
│ └── users_20220413_204956.json
├── ldapsearchbof_logs
│ ├── beacon_202.log
│ ├── beacon_202_no_acl.log
│ ├── beacon_257-objects.log
│ ├── beacon_marvel_ldap_sessions_localgroup.log
│ └── pyldapsearch_redania_objects.log
├── ldapsearchpy_logs
│ ├── ldapsearch_20220413.log
│ ├── ldapsearch_20220414.log
│ └── ldapsearch_516-objects.log
├── netlocalgroupbof_logs
│ └── netlocalgroupbof_redania.log
├── netloggedonbof_logs
│ └── netloggedonbof_redania.log
├── netsessionbof_logs
│ ├── netsessionbof_redania_dns.log
│ └── netsessionbof_redania_netapi.log
└── regsessionbof_logs
│ └── regsessionbof_redania.log
├── test_localgroup_to_computer.py
└── test_session_to_computer.py
/.assets/usage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coffeegist/bofhound/fed5d2b63fc38f178423dfb1f77e2f6e9d337634/.assets/usage.png
--------------------------------------------------------------------------------
/.github/workflows/pytest.yml:
--------------------------------------------------------------------------------
1 | name: PyTest
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | test:
6 | runs-on: ubuntu-latest
7 | container: python:3.9
8 | steps:
9 | - uses: actions/checkout@v3
10 |
11 | - name: Install poetry dependencies
12 | run: pip install poetry
13 |
14 | - name: Install project
15 | run: poetry install
16 |
17 | - name: Run test suite
18 | run: poetry run pytest -v
19 |
--------------------------------------------------------------------------------
/.github/workflows/require-changelog-updates.yml:
--------------------------------------------------------------------------------
1 | name: Require Changelog Updates
2 |
3 | on:
4 | pull_request:
5 | types:
6 | - opened
7 | - synchronize
8 | branches:
9 | - main
10 |
11 | jobs:
12 | check-changelog:
13 | runs-on: ubuntu-latest
14 |
15 | steps:
16 | - name: Checkout Repository
17 | uses: actions/checkout@v4
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Check Changelog
22 | run: |
23 | git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
24 | changed_files=$(git diff --name-only HEAD $(git merge-base HEAD ${{ github.base_ref }}))
25 | if [[ $changed_files != *CHANGELOG.md* ]]; then
26 | echo "::error::No changes to CHANGELOG.md detected. Please update the changelog before merging."
27 | exit 1
28 | fi
29 |
--------------------------------------------------------------------------------
/.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 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105 | __pypackages__/
106 |
107 | # Celery stuff
108 | celerybeat-schedule
109 | celerybeat.pid
110 |
111 | # SageMath parsed files
112 | *.sage.py
113 |
114 | # Environments
115 | .env
116 | .venv
117 | env/
118 | venv/
119 | ENV/
120 | env.bak/
121 | venv.bak/
122 |
123 | # Spyder project settings
124 | .spyderproject
125 | .spyproject
126 |
127 | # Rope project settings
128 | .ropeproject
129 |
130 | # mkdocs documentation
131 | /site
132 |
133 | # mypy
134 | .mypy_cache/
135 | .dmypy.json
136 | dmypy.json
137 |
138 | # Pyre type checker
139 | .pyre/
140 |
141 | # pytype static type analyzer
142 | .pytype/
143 |
144 | # Cython debug symbols
145 | cython_debug/
146 |
147 | # PyCharm
148 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150 | # and can be added to the global gitignore or merged into this file. For a more nuclear
151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152 | #.idea/
153 |
154 | # Test data
155 | !tests/test_data/**/*.log
156 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | ## [0.4.8] - 5/12/2025
3 | ### Fixed
4 | - Check for `operatingsystemservicepack` property to prevent key error - [merge #30](https://github.com/coffeegist/bofhound/pull/30)
5 | - Check for `CertTemplates` property to prevent type error - [merge #31](https://github.com/coffeegist/bofhound/pull/31)
6 |
7 | ## [0.4.7] - 04/29/2025
8 | ### Added
9 | - Ability to handle schemaIdGuids from ADExplorer LDIF output
10 |
11 | ### Fixed
12 | - Improved fix for [#12](https://github.com/coffeegist/bofhound/issues/12) by keying on the `securityIdentifier` attribute on trust objects
13 |
14 | ## [0.4.6] - 04/07/2025
15 | #### Fixed
16 | - Removed log statement clogging debug output [#19](https://github.com/coffeegist/bofhound/issues/19)
17 | - Update deprecated pyproject.toml syntax [#20](https://github.com/coffeegist/bofhound/issues/20)
18 |
19 | ## [0.4.5] - 12/17/2024
20 | #### Added
21 | - Support for pasing ldapsearch BOF results within OutflankC2 log files
22 |
23 | ## [0.4.4] - 12/13/2024
24 | ### Fixed
25 | - Addressed [#13](https://github.com/coffeegist/bofhound/issues/13)
26 | - Catch error is ACL paring fails for an object
27 |
28 | ## [0.4.3] - 10/30/2024
29 | ### Added
30 | - Support for pasing ldapsearch BOF results within Havoc log files
31 |
32 | ### Changed
33 | - Parsers now can inherit from the `LdapSearchBofParser` (since support for other C2s usually still relies on the same BOF) to cut down on code copypasta
34 | - The `GenericParser` class (used to parse local group memberships, session data) is now called from main parsers (`LdapSearchBofParser`, `HavocParser`, etc.) to prevent each logfile from being opened, read, formatted, and parsed twice (each file is now read once and just parsed twice, once for LDAP objects and once for local objects)
35 |
36 | ## [0.4.2] - 10/24/2024
37 | ### Fixed
38 | - Addressed [#12](https://github.com/coffeegist/bofhound/issues/12), an issue with duplicate trusted domain objects
39 |
40 | ## [0.4.1] - 10/22/2024
41 | ### Fixed
42 | - Addressed [#10](https://github.com/coffeegist/bofhound/issues/10), an issue with the `ContainedBy` attribute in output JSON
43 |
44 | ## [0.4.0] - 10/20/2024
45 | ### Added
46 | - Models for ADCS objects and abuse
47 | - AIACAs
48 | - Root CAs
49 | - Enterprise CAs
50 | - Certificate Templates
51 | - Issuance Policies
52 | - NTAuth Stores
53 |
54 | ### Changed
55 | - Split `--all-properties` into 3 levels of properties
56 | - `Standard` to closely mirror object attributes shown by SharpHound/BHCE
57 | - `Member` to include `member` and `memberOf` properties (and a few others)
58 | - `All` to include all properties parsed by bofhound
59 |
60 | ## [0.3.1] - 1/25/2024
61 | ### Fixed
62 | - GPO JSON file not matching JSON definition for BHCE
63 | - `domainsid` property gets set on all GPO objects now (requires domain objects to be queried)
64 |
65 | ## [0.3.0] - 12/27/2023
66 | ### Added
67 | - ADDS model for AD crossRef objects (referrals)
68 | - Models for Local objects (sessions and local group memberships)
69 | - Parsers for registry sessions, privileged sessions, sessions and local group memberships
70 | - ADDS processing logic to tie local group/session data to a computer object
71 |
72 | ## [0.2.1] - 08/09/2023
73 | ### Changed
74 | - Updated output JSON to v5 (BloodHound CE) specs
75 |
76 | ## [0.2.0] - 03/28/2023
77 | ### Added
78 | - New parser to support parsing LDAP Sentinel data from BRc4 logs
79 |
80 | ### Changed
81 | - Modified logic for how group memberships are determined
82 | - Prior method was iterate through DNs in groups' `member` attribute and adding objects with matching DNs
83 | - Since BRc4 does not store DNs in the `member` attibute, added iteration over objects' `memberOf` attribute and add to groups with matching DN (i.e. membership is now calculated from both sides of relationship)
84 |
85 | ## [v0.1.2] - 2/10/2023
86 | ### Changed
87 | - Updated ACL parsing function to current version BloodHound.py
88 | - Updated `typer` and `bloodhound-python` dependencies
89 | - Added the `memberof` attrbute to the common properties displayed for users, computers and groups
90 |
91 | ## [v0.1.1] - 8/11/2022
92 | ### Fixed
93 | - Bug where domain trusts queried more than once would appear duplicated in the BH UI
94 |
95 | ## [v0.1.0] - 6/9/2022
96 | ### Added
97 | - Parsing support for Group Policy Objects
98 | - Parsing support for Organizational Unit objects
99 | - Parsing support for Trusted Domain objects
100 |
101 | ### Fixed
102 | - Bug causing crash when handling non-base64 encoded SchemaIDGUID/nTSecurityDescriptor attributes
103 |
104 | ## [v0.0.1] - 5/9/2022
105 | ### Added
106 | - Prepped for initial release and PyPI package
107 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 4-Clause License
2 |
3 | Copyright (c) 2022, Fortalice Solutions, LLC.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. All advertising materials mentioning features or use of this software must
17 | display the following acknowledgement:
18 | This product includes software developed by Fortalice Solutions, LLC.
19 |
20 | 4. Neither the name of the copyright holder nor the names of its
21 | contributors may be used to endorse or promote products derived from
22 | this software without specific prior written permission.
23 |
24 | THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
25 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
26 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
27 | EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
29 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
30 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
31 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
32 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
33 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | _____________________________ __ __ ______ __ __ __ __ _______
3 | | _ / / __ / | ____/| | | | / __ \ | | | | | \ | | | \
4 | | |_) | | | | | | |__ | |__| | | | | | | | | | | \| | | .--. |
5 | | _ < | | | | | __| | __ | | | | | | | | | | . ` | | | | |
6 | | |_) | | `--' | | | | | | | | `--' | | `--' | | |\ | | '--' |
7 | |______/ \______/ |__| |__| |___\_\________\_\________\|__| \___\|_________\
8 |
9 | << @coffeegist | @Tw1sm >>
10 | ```
11 |
12 |
13 |
14 | 
15 | 
16 |
17 |
18 | BOFHound is an offline BloodHound ingestor and LDAP result parser compatible with TrustedSec's [ldapsearch BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF), the Python adaptation, [pyldapsearch](https://github.com/fortalice/pyldapsearch) and Brute Ratel's [LDAP Sentinel](https://bruteratel.com/tabs/commander/badgers/#ldapsentinel). ldapsearch BOF logs can also be parsed from [Havoc](https://github.com/HavocFramework/Havoc) or OutflankC2 logs.
19 |
20 | By parsing log files generated by the aforementioned tools, BOFHound allows operators to utilize BloodHound's beloved interface while maintaining full control over the LDAP queries being run and the spped at which they are executed. This leaves room for operator discretion to account for potential honeypot accounts, expensive LDAP query thresholds and other detection mechanisms designed with the traditional, automated BloodHound collectors in mind.
21 |
22 | Check this [PR](https://github.com/trustedsec/CS-Situational-Awareness-BOF/pull/114) to the SA BOF repo for BOFs that collect session and local group membership data and can be parsed by BOFHound.
23 |
24 | ### References
25 |
26 | Blog Posts:
27 |
28 | | Title| Date|
29 | |------|-----|
30 | | [*BOFHound: AD CS Integration*](https://medium.com/specter-ops-posts/bofhound-ad-cs-integration-91b706bc7958) | Oct 30, 2024 |
31 | | [*BOFHound: Session Integration*](https://posts.specterops.io/bofhound-session-integration-7b88b6f18423) | Jan 30, 2024 |
32 | | [*Granularize Your AD Recon Game Part 2*](https://www.fortalicesolutions.com/posts/granularize-your-active-directory-reconnaissance-game-part-2) | Jun 15, 2022 |
33 | | [*Granularize Your AD Recon Game*](https://www.fortalicesolutions.com/posts/bofhound-granularize-your-active-directory-reconnaissance-game) | May 10, 2022 |
34 |
35 | Presentations:
36 |
37 | | Conference| Materials| Date|
38 | |-----------|----------|-----|
39 | | *SO-CON 2024*| [Slides](https://github.com/SpecterOps/presentations/blob/main/SO-CON%202024/Matt%20Creel%20%26%20Adam%20Brown%20-%20Manually%20Enumerating%20AD%20Attack%20Paths%20with%20BOFHound/Matt%20Creel%20and%20Adam%20Brown%20-%20Manually%20Enumerating%20AD%20Attack%20Paths%20With%20BOFHound%20-%20SO-CON%202024.pdf) & [Recording](https://www.youtube.com/watch?v=Xxm4YktSKVY)| Mar 11, 2024|
40 |
41 | # Installation
42 | BOFHound can be installed with `pip3 install bofhound` or by cloning this repository and running `pip3 install .`
43 |
44 | # Usage
45 | ```
46 | Usage: bofhound [OPTIONS]
47 |
48 | Generate BloodHound compatible JSON from logs written by ldapsearch BOF, pyldapsearch and Brute Ratel's
49 | LDAP Sentinel
50 |
51 | Generate BloodHound compatible JSON from logs written by ldapsearch BOF, pyldapsearch and Brute
52 | Ratel's LDAP Sentinel
53 |
54 | ╭─ Options ──────────────────────────────────────────────────────────────────────────────────────╮
55 | │ --input -i TEXT Directory or file containing │
56 | │ logs of ldapsearch results │
57 | │ [default: │
58 | │ /opt/cobaltstrike/logs] │
59 | │ --output -o TEXT Location to export bloodhound │
60 | │ files │
61 | │ [default: .] │
62 | │ --properties-level -p [Standard|Member|All] Change the verbosity of │
63 | │ properties exported to JSON: │
64 | │ Standard - Common BH properties │
65 | │ | Member - Includes MemberOf and │
66 | │ Member | All - Includes all │
67 | │ properties │
68 | │ [default: Member] │
69 | │ --parser [ldapsearch|BRC4|Havoc| Parser to use for log files. │
70 | │ OutflankC2] ldapsearch parser (default) │
71 | │ supports ldapsearch BOF logs │
72 | │ from Cobalt Strike and │
73 | │ pyldapsearch logs │
74 | │ [default: ldapsearch] │
75 | │ --debug Enable debug output │
76 | │ --zip -z Compress the JSON output files │
77 | │ into a zip archive │
78 | │ --help -h Show this message and exit. │
79 | ╰────────────────────────────────────────────────────────────────────────────────────────────────╯
80 |
81 |
82 | ```
83 |
84 | ## Example Usage
85 | Parse ldapseach BOF results from Cobalt Strike logs (`/opt/cobaltstrike/logs` by default) to /data/
86 | ```
87 | bofhound -o /data/
88 | ```
89 |
90 | Parse pyldapsearch logs and only include all properties (vs other property levels)
91 | ```
92 | bofhound -i ~/.pyldapsearch/logs/ --properties-level all
93 | ```
94 |
95 | Parse LDAP Sentinel data from BRc4 logs (will change default input path to `/opt/bruteratel/logs`)
96 | ```
97 | bofhound --parser brc4
98 | ```
99 |
100 | Parse Havoc loot logs (will change default input path to `/opt/havoc/data/loot`) and zip the resulting JSON files
101 | ```
102 | bofhound --parser havoc --zip
103 | ```
104 |
105 | # ldapsearch
106 | Specify `*,ntsecuritydescriptor` as the attributes to return to be able to parse ACL edges. You are missing a ton of data if you don't include this in your `ldapsearch` queries!
107 |
108 | #### Required Data
109 | The following attributes are required for proper functionality:
110 | ```
111 | samaccounttype
112 | distinguishedname
113 | objectsid
114 | ```
115 |
116 | Some object classes rely on domain objects being populated within BOFHound. Domains can be queried with either of the following commands
117 | ```
118 | ldapsearch (objectclass=domain) *,ntsecuritydescriptor
119 | ldapsearch (distinguishedname=DC=windomain,DC=local) *,ntsecuritydescriptor
120 | ```
121 |
122 | ## Example ldapsearch Queries
123 | ```
124 | # Get All the Data (Maybe Run BloodHound Instead?)
125 | ldapsearch (objectclass=*) *,ntsecuritydescriptor
126 |
127 | # Retrieve All Schema Info
128 | ldapsearch (schemaIDGUID=*) name,schemaidguid 0 3 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
129 |
130 | # Retrieve Only the ms-Mcs-AdmPwd schemaIDGUID
131 | ldapsearch (name=ms-mcs-admpwd) name,schemaidguid 1 3 "" CN=Schema,CN=Configuration,DC=windomain,DC=local
132 |
133 | # Retrieve Domain NetBIOS Names (useful if collecting data via `netsession2/netloggedon2` BOFs)
134 | ldapsearch (netbiosname=*) * 0 3 "" "CN=Partitions,CN=Configuration,DC=windomain,DC=local"
135 |
136 | # Unroll a group's nested members
137 | ldapsearch (memberOf:1.2.840.113556.1.4.1941:=CN=TargetGroup,CN=Users,DC=windomain,DC=local) *,ntsecuritydescriptor
138 |
139 | # Query domain trusts
140 | ldapsearch (objectclass=trusteddomain) *,ntsecuritydescriptor
141 |
142 | # Query across a trust
143 | ldapsearch (objectclass=domain) *,ntsecuritydescriptor 0 3 dc1.trusted.windomain.local "DC=TRUSTED,DC=WINDOMAIN,DC=LOCAL"
144 |
145 | #####
146 | # Queries below populate objects for AD CS parsing
147 |
148 | # Query the domain object
149 | ldapsearch (objectclass=domain) *,ntsecuritydescriptor
150 |
151 | # Query Enterprise CAs
152 | ldapsearch (objectclass=pKIEnrollmentService) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
153 |
154 | # Query AIACAs, Root CAs and NTAuth Stores
155 | ldapsearch (objectclass=certificationAuthority) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
156 |
157 | # Query Certificate Templates
158 | ldapsearch (objectclass=pKICertificateTemplate) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
159 |
160 | # Query Issuance Policies
161 | ldapsearch (objectclass=msPKI-Enterprise-Oid) *,ntsecuritydescriptor 0 3 “” “CN=Configuration,DC=domain,DC=local”
162 | ```
163 |
164 | # Versions
165 | Check the tagged releases to download a specific version
166 | - v0.4.0 and onward support parsing AD CS objects and edges
167 | - v0.3.0 and onward support session/local group data
168 | - v0.2.1 and onward are compatible with BloodHound CE
169 | - v0.2.0 is the last release supporting BloodHound Legacy
170 |
171 | # Development
172 | bofhound uses Poetry to manage dependencies. Install from source and setup for development with:
173 |
174 | ```shell
175 | git clone https://github.com/fortalice/bofhound
176 | cd bofhound
177 | poetry install
178 | poetry run bofhound --help
179 | ```
180 |
181 | # References and Credits
182 | - [@_dirkjan](https://twitter.com/_dirkjan) (and other contributors) for [BloodHound.py](https://github.com/fox-it/BloodHound.py)
183 | - TrustedSec for [CS-Situational-Awareness-BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF)
184 | - [P-aLu](https://github.com/P-aLu) for collaboration on bofhoud's [AD CS support](https://github.com/coffeegist/bofhound/pull/8)
185 |
--------------------------------------------------------------------------------
/bofhound/__init__.py:
--------------------------------------------------------------------------------
1 | from rich.console import Console
2 |
3 | console = Console()
4 |
--------------------------------------------------------------------------------
/bofhound/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | import logging
4 | import typer
5 | import glob
6 | from bofhound.parsers import LdapSearchBofParser, Brc4LdapSentinelParser, HavocParser, ParserType, OutflankC2JsonParser
7 | from bofhound.writer import BloodHoundWriter
8 | from bofhound.ad import ADDS
9 | from bofhound.local import LocalBroker
10 | from bofhound import console
11 | from bofhound.ad.helpers import PropertiesLevel
12 |
13 | app = typer.Typer(
14 | add_completion=False,
15 | rich_markup_mode="rich",
16 | context_settings={'help_option_names': ['-h', '--help']}
17 | )
18 |
19 | @app.command()
20 | def main(
21 | input_files: str = typer.Option("/opt/cobaltstrike/logs", "--input", "-i", help="Directory or file containing logs of ldapsearch results"),
22 | output_folder: str = typer.Option(".", "--output", "-o", help="Location to export bloodhound files"),
23 | properties_level: PropertiesLevel = typer.Option(PropertiesLevel.Member.value, "--properties-level", "-p", case_sensitive=False, help='Change the verbosity of properties exported to JSON: Standard - Common BH properties | Member - Includes MemberOf and Member | All - Includes all properties'),
24 | parser: ParserType = typer.Option(ParserType.LdapsearchBof.value, "--parser", case_sensitive=False, help="Parser to use for log files. ldapsearch parser (default) supports ldapsearch BOF logs from Cobalt Strike and pyldapsearch logs"),
25 | debug: bool = typer.Option(False, "--debug", help="Enable debug output"),
26 | zip_files: bool = typer.Option(False, "--zip", "-z", help="Compress the JSON output files into a zip archive")):
27 | """
28 | Generate BloodHound compatible JSON from logs written by ldapsearch BOF, pyldapsearch and Brute Ratel's LDAP Sentinel
29 | """
30 |
31 | if debug:
32 | logging.getLogger().setLevel(logging.DEBUG)
33 | else:
34 | logging.getLogger().setLevel(logging.INFO)
35 |
36 | banner()
37 |
38 | # default to Cobalt logfile naming format
39 | logfile_name_format = "beacon*.log"
40 |
41 | match parser:
42 |
43 | case ParserType.LdapsearchBof:
44 | logging.debug('Using ldapsearch parser')
45 | parser = LdapSearchBofParser
46 |
47 | case ParserType.BRC4:
48 | logging.debug('Using Brute Ratel parser')
49 | parser = Brc4LdapSentinelParser
50 | logfile_name_format = "b-*.log"
51 | if input_files == "/opt/cobaltstrike/logs":
52 | input_files = "/opt/bruteratel/logs"
53 |
54 | case ParserType.HAVOC:
55 | logging.debug('Using Havoc parser')
56 | parser = HavocParser
57 | logfile_name_format = "Console_*.log"
58 | if input_files == "/opt/cobaltstrike/logs":
59 | input_files = "/opt/havoc/data/loot"
60 |
61 | case ParserType.OUTFLANKC2:
62 | logging.debug('Using OutflankC2 parser')
63 | parser = OutflankC2JsonParser
64 | logfile_name_format = "*.json"
65 |
66 | case _:
67 | raise ValueError(f"Unknown parser type: {parser}")
68 |
69 | if os.path.isfile(input_files):
70 | cs_logs = [input_files]
71 | logging.debug(f"Log file explicitly provided {input_files}")
72 | elif os.path.isdir(input_files):
73 | # recurisively get a list of all .log files in the input directory, sorted by last modified time
74 | cs_logs = glob.glob(f"{input_files}/**/{logfile_name_format}", recursive=True)
75 | if len(cs_logs) == 0:
76 | # check for ldapsearch python logs
77 | cs_logs = glob.glob(f"{input_files}/pyldapsearch*.log", recursive=True)
78 |
79 | cs_logs.sort(key=os.path.getmtime)
80 |
81 | if len(cs_logs) == 0:
82 | logging.error(f"No log files found in {input_files}!")
83 | return
84 | else:
85 | logging.info(f"Located {len(cs_logs)} beacon log files")
86 | else:
87 | logging.error(f"Could not find {input_files} on disk")
88 | sys.exit(-1)
89 |
90 | parsed_ldap_objects = []
91 | parsed_local_objects = []
92 | with console.status(f"", spinner="aesthetic") as status:
93 | for log in cs_logs:
94 | status.update(f" [bold] Parsing {log}")
95 | formatted_data = parser.prep_file(log)
96 | new_objects = parser.parse_data(formatted_data)
97 |
98 | # jank insert to reparse outflank logs for local data
99 | if parser == OutflankC2JsonParser:
100 | new_local_objects = parser.parse_local_objects(log)
101 | else:
102 | new_local_objects = parser.parse_local_objects(formatted_data)
103 |
104 | logging.debug(f"Parsed {log}")
105 | logging.debug(f"Found {len(new_objects)} objects in {log}")
106 | parsed_ldap_objects.extend(new_objects)
107 | parsed_local_objects.extend(new_local_objects)
108 |
109 | logging.info(f"Parsed {len(parsed_ldap_objects)} LDAP objects from {len(cs_logs)} log files")
110 | logging.info(f"Parsed {len(parsed_local_objects)} local group/session objects from {len(cs_logs)} log files")
111 |
112 | ad = ADDS()
113 | broker = LocalBroker()
114 |
115 | logging.info("Sorting parsed objects by type...")
116 | ad.import_objects(parsed_ldap_objects)
117 | broker.import_objects(parsed_local_objects, ad.DOMAIN_MAP.values())
118 |
119 | logging.info(f"Parsed {len(ad.users)} Users")
120 | logging.info(f"Parsed {len(ad.groups)} Groups")
121 | logging.info(f"Parsed {len(ad.computers)} Computers")
122 | logging.info(f"Parsed {len(ad.domains)} Domains")
123 | logging.info(f"Parsed {len(ad.trustaccounts)} Trust Accounts")
124 | logging.info(f"Parsed {len(ad.ous)} OUs")
125 | logging.info(f"Parsed {len(ad.containers)} Containers")
126 | logging.info(f"Parsed {len(ad.gpos)} GPOs")
127 | logging.info(f"Parsed {len(ad.enterprisecas)} Enterprise CAs")
128 | logging.info(f"Parsed {len(ad.aiacas)} AIA CAs")
129 | logging.info(f"Parsed {len(ad.rootcas)} Root CAs")
130 | logging.info(f"Parsed {len(ad.ntauthstores)} NTAuth Stores")
131 | logging.info(f"Parsed {len(ad.issuancepolicies)} Issuance Policies")
132 | logging.info(f"Parsed {len(ad.certtemplates)} Cert Templates")
133 | logging.info(f"Parsed {len(ad.schemas)} Schemas")
134 | logging.info(f"Parsed {len(ad.CROSSREF_MAP)} Referrals")
135 | logging.info(f"Parsed {len(ad.unknown_objects)} Unknown Objects")
136 | logging.info(f"Parsed {len(broker.sessions)} Sessions")
137 | logging.info(f"Parsed {len(broker.privileged_sessions)} Privileged Sessions")
138 | logging.info(f"Parsed {len(broker.registry_sessions)} Registry Sessions")
139 | logging.info(f"Parsed {len(broker.local_group_memberships)} Local Group Memberships")
140 |
141 | ad.process()
142 | ad.process_local_objects(broker)
143 |
144 | BloodHoundWriter.write(
145 | output_folder,
146 | domains=ad.domains,
147 | computers=ad.computers,
148 | users=ad.users,
149 | groups=ad.groups,
150 | ous=ad.ous,
151 | containers=ad.containers,
152 | gpos=ad.gpos,
153 | enterprisecas=ad.enterprisecas,
154 | aiacas=ad.aiacas,
155 | rootcas=ad.rootcas,
156 | ntauthstores=ad.ntauthstores,
157 | issuancepolicies=ad.issuancepolicies,
158 | certtemplates = ad.certtemplates,
159 | properties_level=properties_level,
160 | zip_files=zip_files
161 | )
162 |
163 |
164 | def banner():
165 | print('''
166 | _____________________________ __ __ ______ __ __ __ __ _______
167 | | _ / / __ / | ____/| | | | / __ \\ | | | | | \\ | | | \\
168 | | |_) | | | | | | |__ | |__| | | | | | | | | | | \\| | | .--. |
169 | | _ < | | | | | __| | __ | | | | | | | | | | . ` | | | | |
170 | | |_) | | `--' | | | | | | | | `--' | | `--' | | |\\ | | '--' |
171 | |______/ \\______/ |__| |__| |___\\_\\________\\_\\________\\|__| \\___\\|_________\\
172 |
173 | << @coffeegist | @Tw1sm >>
174 | ''')
175 |
176 |
177 | if __name__ == "__main__":
178 | app(prog_name="bofhound")
179 |
--------------------------------------------------------------------------------
/bofhound/ad/__init__.py:
--------------------------------------------------------------------------------
1 | from .adds import ADDS
--------------------------------------------------------------------------------
/bofhound/ad/helpers/__init__.py:
--------------------------------------------------------------------------------
1 | from .trustdirection import TrustDirection
2 | from .trusttype import TrustType
3 | from .propertieslevel import PropertiesLevel
--------------------------------------------------------------------------------
/bofhound/ad/helpers/propertieslevel.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class PropertiesLevel(Enum):
4 | Standard = 'Standard'
5 | Member = 'Member'
6 | All = 'All'
--------------------------------------------------------------------------------
/bofhound/ad/helpers/trustdirection.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class TrustDirection(Enum):
4 | Disabled = 0
5 | Inbound = 1
6 | Outbound = 2
7 | Bidirectional = 3
--------------------------------------------------------------------------------
/bofhound/ad/helpers/trusttype.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class TrustType(Enum):
4 | ParentChild = 0
5 | CrossLink = 1
6 | Forest = 2
7 | External = 3
8 | Unknown = 4
--------------------------------------------------------------------------------
/bofhound/ad/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .bloodhound_domain import BloodHoundDomain
2 | from .bloodhound_computer import BloodHoundComputer
3 | from .bloodhound_user import BloodHoundUser
4 | from .bloodhound_group import BloodHoundGroup
5 | from .bloodhound_object import BloodHoundObject
6 | from .bloodhound_schema import BloodHoundSchema
7 | from .bloodhound_ou import BloodHoundOU
8 | from .bloodhound_container import BloodHoundContainer
9 | from .bloodhound_gpo import BloodHoundGPO
10 | from .bloodhound_enterpriseca import BloodHoundEnterpriseCA
11 | from .bloodhound_rootca import BloodHoundRootCA
12 | from .bloodhound_aiaca import BloodHoundAIACA
13 | from .bloodhound_ntauthstore import BloodHoundNTAuthStore
14 | from .bloodhound_issuancepolicy import BloodHoundIssuancePolicy
15 | from .bloodhound_certtemplate import BloodHoundCertTemplate
16 | from .bloodhound_domaintrust import BloodHoundDomainTrust
17 | from .bloodhound_crossref import BloodHoundCrossRef
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_aiaca.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 |
7 | class BloodHoundAIACA(BloodHoundObject):
8 |
9 | GUI_PROPERTIES = [
10 | 'domain', 'name', 'distinguishedname', 'domainsid', 'isaclprotected',
11 | 'description', 'whencreated', 'crosscertificatepair', 'hascrosscertificatepair',
12 | 'certthumbprint', 'certname', 'certchain', 'hasbasicconstraints',
13 | 'basicconstraintpathlength'
14 | ]
15 |
16 | COMMON_PROPERTIES = [
17 | ]
18 |
19 | def __init__(self, object):
20 | super().__init__(object)
21 |
22 | self._entry_type = "AIACA"
23 | self.ContainedBy = {}
24 | self.IsACLProtected = False
25 | self.IsDeleted = False
26 | self.x509Certificate = None
27 |
28 | if 'objectguid' in object.keys():
29 | self.ObjectIdentifier = object.get("objectguid")
30 |
31 | if 'distinguishedname' in object.keys():
32 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
33 | self.Properties['domain'] = domain
34 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
35 |
36 | if 'description' in object.keys():
37 | self.Properties['description'] = object.get('description')
38 | else:
39 | self.Properties['description'] = None
40 |
41 | if 'name' in object.keys():
42 | if 'domain' in self.Properties.keys():
43 | self.Properties['name'] = object.get('name').upper() + "@" + self.Properties['domain'].upper()
44 |
45 | if 'crosscertificatepair' in object.keys():
46 | self.Properties['crosscertificatepair'] = object.get('crosscertificatepair')
47 | self.Properties['hascrosscertificatepair'] = True
48 | else:
49 | self.Properties['crosscertificatepair'] = []
50 | self.Properties['hascrosscertificatepair'] = False
51 |
52 | if 'cacertificate' in object.keys():
53 | self.parse_cacertificate(object)
54 |
55 | if 'ntsecuritydescriptor' in object.keys():
56 | self.RawAces = object['ntsecuritydescriptor']
57 |
58 |
59 | def to_json(self, properties_level):
60 | self.Properties['isaclprotected'] = self.IsACLProtected
61 | data = super().to_json(properties_level)
62 |
63 | data["Aces"] = self.Aces
64 | data["IsDeleted"] = self.IsDeleted
65 | data["IsACLProtected"] = self.IsACLProtected
66 | data["ObjectIdentifier"] = self.ObjectIdentifier
67 | data["ContainedBy"] = self.ContainedBy
68 |
69 | return data
70 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_computer.py:
--------------------------------------------------------------------------------
1 | import calendar
2 | from datetime import datetime
3 | from bloodhound.ad.utils import ADUtils, LDAP_SID
4 | from .bloodhound_object import BloodHoundObject
5 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
6 | import logging
7 |
8 | class BloodHoundComputer(BloodHoundObject):
9 |
10 | GUI_PROPERTIES = [
11 | 'domain', 'name', 'distinguishedname', 'domainsid', 'samaccountname',
12 | 'haslaps', 'isaclprotected', 'description', 'whencreated', 'enabled',
13 | 'unconstraineddelegation', 'trustedtoauth', 'isdc', 'lastlogon', 'lastlogontimestamp',
14 | 'pwdlastset', 'serviceprincipalnames', 'email', 'operatingsystem', 'sidhistory'
15 | ]
16 |
17 | COMMON_PROPERTIES = [
18 | 'useraccountcontrol', 'dnshostname', 'samaccounttype', 'primarygroupid',
19 | 'msds-allowedtodelegateto', 'operatingsystemservicepack',
20 | 'msds-allowedtoactonbehalfofotheridentity', 'ms-mcs-admpwdexpirationtime',
21 | 'memberof'
22 | ]
23 |
24 | LOCAL_GROUP_SIDS = {
25 | "administrators": 544,
26 | "remote desktop users": 555,
27 | "remote management users": 580,
28 | "distributed com users": 562
29 | }
30 |
31 | def __init__(self, object):
32 | super().__init__(object)
33 |
34 | self._entry_type = "Computer"
35 | self.not_collected = {
36 | "Collected": False,
37 | "FailureReason": None,
38 | "Results": []
39 | }
40 | self.uac = None
41 | self.IsACLProtected = False
42 | self.IsDeleted = False
43 | self.hostname = object.get('dnshostname', None)
44 | self.PrimaryGroupSid = self.get_primary_membership(object) # Returns none if non-existent
45 | self.sessions = None #['not currently supported by bofhound']
46 | self.AllowedToDelegate = []
47 | self.MemberOfDNs = []
48 | self.sessions = []
49 | self.ContainedBy = {}
50 | self.privileged_sessions = []
51 | self.registry_sessions = []
52 | self.local_group_members = {} # {group_name: [{member_sid, member_type}]}
53 |
54 | if 'dnshostname' in object.keys():
55 | self.hostname = object.get('dnshostname', None)
56 | self.Properties['name'] = self.hostname.upper()
57 | logging.debug(f"Reading Computer object {ColorScheme.computer}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
58 |
59 | if 'msds-allowedtodelegateto' in object.keys():
60 | self.AllowedToDelegate = object.get('msds-allowedtodelegateto').split(', ')
61 |
62 | if 'useraccountcontrol' in object.keys():
63 | self.uac = int(object.get('useraccountcontrol'))
64 | self.Properties['unconstraineddelegation'] = self.uac & 0x00080000 == 0x00080000
65 | self.Properties['enabled'] = self.uac & 2 == 0
66 | self.Properties['trustedtoauth'] = self.uac & 0x01000000 == 0x01000000
67 | self.Properties['isdc'] = self.uac & 0x2000 == 0x2000
68 |
69 | if 'operatingsystem' in object.keys():
70 | self.Properties['operatingsystem'] = object.get('operatingsystem', 'Unknown')
71 |
72 | if 'operatingsystemservicepack' in object.keys() and 'operatingsystem' in self.Properties:
73 | self.Properties['operatingsystem'] += f' {object.get("operatingsystemservicepack")}'
74 |
75 | if 'sidhistory' in object.keys():
76 | self.Properties['sidhistory'] = [LDAP_SID(bsid).formatCanonical() for bsid in object.get('sidhistory', [])]
77 | else:
78 | self.Properties['sidhistory'] = []
79 |
80 | if 'distinguishedname' in object.keys():
81 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
82 | self.Properties['domain'] = domain
83 | if 'samaccountname' in object.keys() and 'dnshostname' not in object.keys():
84 | samacctname = object.get("samaccountname")
85 | if samacctname.endswith("$"):
86 | name = f'{samacctname[:-1]}.{domain}'.upper()
87 | else:
88 | name = f'{samacctname}.{domain}'.upper()
89 | self.Properties["name"] = name
90 | logging.debug(f"Reading Computer object {ColorScheme.computer}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
91 |
92 | # TODO: HighValue / AdminCount
93 | self.Properties['highvalue'] = False
94 |
95 | if 'ms-mcs-admpwdexpirationtime' in object.keys():
96 | self.Properties['haslaps'] = True
97 | else:
98 | self.Properties['haslaps'] = False
99 |
100 | if 'lastlogontimestamp' in object.keys():
101 | self.Properties['lastlogontimestamp'] = ADUtils.win_timestamp_to_unix(
102 | int(object.get('lastlogontimestamp'))
103 | )
104 |
105 | if 'lastlogon' in object.keys():
106 | self.Properties['lastlogon'] = ADUtils.win_timestamp_to_unix(
107 | int(object.get('lastlogon'))
108 | )
109 |
110 | if 'pwdlastset' in object.keys():
111 | self.Properties['pwdlastset'] = ADUtils.win_timestamp_to_unix(
112 | int(object.get('pwdlastset'))
113 | )
114 |
115 | if 'serviceprincipalname' in object.keys():
116 | self.Properties['serviceprincipalnames'] = object.get('serviceprincipalname').split(', ')
117 |
118 | if 'description' in object.keys():
119 | self.Properties['description'] = object.get('description')
120 |
121 | if 'email' in object.keys():
122 | self.Properties['email'] = object.get('email')
123 |
124 | if 'samaccounttype' in object.keys():
125 | self.Properties['samaccounttype'] = object.get('samaccounttype')
126 |
127 | if 'ntsecuritydescriptor' in object.keys():
128 | self.RawAces = object['ntsecuritydescriptor']
129 |
130 | if 'memberof' in object.keys():
131 | self.MemberOfDNs = [f'CN={dn.upper()}' for dn in object.get('memberof').split(', CN=')]
132 | if len(self.MemberOfDNs) > 0:
133 | self.MemberOfDNs[0] = self.MemberOfDNs[0][3:]
134 |
135 | if 'email' in object.keys():
136 | self.Properties['email'] = object.get('email')
137 | else:
138 | self.Properties['email'] = None
139 |
140 | if 'description' in object.keys():
141 | self.Properties['description'] = object.get('description')
142 | else:
143 | self.Properties['description'] = None
144 |
145 | def to_json(self, properties_level):
146 | self.Properties['msds-allowedtodelegateto'] = self.AllowedToDelegate
147 | self.Properties['isaclprotected'] = self.IsACLProtected
148 | data = super().to_json(properties_level)
149 | data["Sessions"] = self.format_session_json(self.sessions)
150 | data["PrivilegedSessions"] = self.format_session_json(self.privileged_sessions)
151 | data["RegistrySessions"] = self.format_session_json(self.registry_sessions)
152 | data["ObjectIdentifier"] = self.ObjectIdentifier
153 | data["PrimaryGroupSID"] = self.PrimaryGroupSid
154 | data["AllowedToDelegate"] = self.AllowedToDelegate
155 | data["AllowedToAct"] = []
156 | data["HasSidHistory"] = self.Properties.get("sidhistory", [])
157 | data["DumpSMSAPassword"] = []
158 | data["LocalGroups"] = self.format_local_group_json()
159 | data["UserRights"] = []
160 | data["Status"] = None
161 | data["IsDeleted"] = self.IsDeleted
162 | data["ContainedBy"] = self.ContainedBy
163 | data["Aces"] = self.Aces
164 | data["IsACLProtected"] = self.IsACLProtected
165 | data["IsDC"] = self.Properties["isdc"]
166 |
167 | return data
168 |
169 |
170 | def format_session_json(self, results):
171 | if len(results) == 0:
172 | return self.not_collected
173 |
174 | return {
175 | "Collected": True,
176 | "FailureReason": None,
177 | "Results": results
178 | }
179 |
180 |
181 | def format_local_group_json(self):
182 | if len(self.local_group_members) == 0:
183 | return []
184 |
185 | data = []
186 |
187 | hostname = self.hostname if self.hostname is not None else self.Properties['name']
188 |
189 | for group_name, members in self.local_group_members.items():
190 | data.append({
191 | "ObjectIdentifier": f"{self.ObjectIdentifier}-{BloodHoundComputer.LOCAL_GROUP_SIDS[group_name]}",
192 | "Name": f"{group_name}@{hostname}".upper(),
193 | "Results": members,
194 | "Collected": True,
195 | "FailureReason": None
196 | })
197 |
198 | return data
199 |
200 |
201 | # check if a session host's fully qualified hostname matches
202 | # the computer's dnshostname attribute
203 | def matches_dnshostname(self, session_host_fqdn):
204 | if self.Properties.get('dnshostname', '').upper() == session_host_fqdn.upper():
205 | return True
206 | return False
207 |
208 |
209 | # check if a session host's hostname matches the computer's samaccountname
210 | def matches_samaccountname(self, session_hostname):
211 | if self.Properties['samaccountname'].upper() == session_hostname.upper() + '$':
212 | return True
213 | return False
214 |
215 |
216 | # add a session to the computer object
217 | def add_session(self, user_sid, session_type):
218 | session = {
219 | "UserSID": user_sid,
220 | "ComputerSID": self.ObjectIdentifier,
221 | }
222 |
223 | if session_type == 'privileged':
224 | self.privileged_sessions.append(session)
225 | elif session_type == 'registry':
226 | self.registry_sessions.append(session)
227 | elif session_type == 'session':
228 | self.sessions.append(session)
229 |
230 |
231 | # add a local group member
232 | def add_local_group_member(self, member_sid, member_type, group_name):
233 | member = {
234 | "ObjectIdentifier": member_sid,
235 | "ObjectType": member_type
236 | }
237 |
238 | if group_name.lower() not in self.local_group_members.keys():
239 | self.local_group_members[group_name.lower()] = [ member ]
240 | else:
241 | self.local_group_members[group_name.lower()].append(member)
242 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_container.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundContainer(BloodHoundObject):
7 |
8 | GUI_PROPERTIES = [
9 | 'domain', 'name', 'distinguishedname', 'domainsid', 'highvalue', 'isaclprotected'
10 | ]
11 |
12 | COMMON_PROPERTIES = [
13 | ]
14 |
15 | def __init__(self, object):
16 | super().__init__(object)
17 |
18 | self._entry_type = "Container"
19 | self.ContainedBy = {}
20 | self.Properties["blocksinheritance"] = False
21 |
22 | if 'objectguid' in object.keys():
23 | self.ObjectIdentifier = object.get('objectguid').upper()
24 |
25 | if 'distinguishedname' in object.keys() and 'ou' in object.keys():
26 | self.Properties["domain"] = ADUtils.ldap2domain(object.get('distinguishedname').upper())
27 | self.Properties["name"] = f"{object.get('name').upper()}@{self.Properties['domain']}"
28 | logging.debug(f"Reading Container object {ColorScheme.ou}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
29 |
30 | self.Properties['highvalue'] = False
31 |
32 | if 'ntsecuritydescriptor' in object.keys():
33 | self.RawAces = object['ntsecuritydescriptor']
34 |
35 | self.Properties["highvalue"] = False
36 |
37 | self.Aces = []
38 | self.ChildObjects = []
39 | self.IsDeleted = False
40 | self.IsACLProtected = False
41 |
42 |
43 | def to_json(self, properties_level):
44 | self.Properties['isaclprotected'] = self.IsACLProtected
45 | ou = super().to_json(properties_level)
46 |
47 | ou["ObjectIdentifier"] = self.ObjectIdentifier
48 | ou["ContainedBy"] = self.ContainedBy
49 | ou["Aces"] = self.Aces
50 | ou["ChildObjects"] = self.ChildObjects
51 | ou["IsDeleted"] = self.IsDeleted
52 | ou["IsACLProtected"] = self.IsACLProtected
53 |
54 | return ou
55 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_crossref.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
2 | import logging
3 |
4 | class BloodHoundCrossRef(object):
5 |
6 | def __init__(self, object):
7 | self.netBiosName = None
8 | self.nCName = None
9 | self.distinguishedName = None
10 |
11 | if 'netbiosname' in object.keys() and 'ncname' in object.keys() and 'distinguishedname' in object.keys():
12 | self.netBiosName = object.get('netbiosname')
13 | self.nCName = object.get('ncname').upper()
14 | self.distinguishedName = object.get('distinguishedname').upper()
15 | logging.debug(f"Reading CrossRef object {ColorScheme.schema}{self.distinguishedName}[/]", extra=OBJ_EXTRA_FMT)
16 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_domain.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundDomain(BloodHoundObject):
7 |
8 | GUI_PROPERTIES = [
9 | 'distinguishedname', 'domainsid', 'description', 'whencreated',
10 | 'functionallevel', 'domain', 'isaclprotected', 'collected',
11 | 'name'
12 | ]
13 |
14 | COMMON_PROPERTIES = [
15 | ]
16 |
17 | def __init__(self, object):
18 | super().__init__(object)
19 |
20 | self._entry_type = "Domain"
21 | self.GPLinks = []
22 | self.ContainedBy = {}
23 | level_id = object.get('msds-behavior-version', 0)
24 | try:
25 | functional_level = ADUtils.FUNCTIONAL_LEVELS[int(level_id)]
26 | except KeyError:
27 | functional_level = 'Unknown'
28 |
29 | dc = None
30 |
31 | self.Properties['collected'] = True
32 |
33 | if 'distinguishedname' in object.keys():
34 | self.Properties["name"] = ADUtils.ldap2domain(object.get('distinguishedname').upper())
35 | self.Properties["domain"] = self.Properties["name"]
36 | dc = BloodHoundObject.get_domain_component(object.get('distinguishedname').upper())
37 | logging.debug(f"Reading Domain object {ColorScheme.domain}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
38 |
39 | if 'objectsid' in object.keys():
40 | self.Properties["domainsid"] = object.get('objectsid')
41 |
42 | if 'distinguishedname' in object.keys():
43 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
44 |
45 | if 'description' in object.keys():
46 | self.Properties["description"] = object.get('description')
47 | else:
48 | self.Properties["description"] = None
49 |
50 | if 'ntsecuritydescriptor' in object.keys():
51 | self.RawAces = object['ntsecuritydescriptor']
52 |
53 | if 'gplink' in object.keys():
54 | # this is gross - not sure why gplink is coming in without a colon
55 | # (even from logs in test folder) but will hunt down later if it's a problem
56 | links = object.get('gplink').replace('LDAP//', 'LDAP://')
57 |
58 | # [['DN1', 'GPLinkOptions1'], ['DN2', 'GPLinkOptions2'], ...]
59 | self.GPLinks = [link.upper()[:-1].split(';') for link in links.split('[LDAP://')][1:]
60 |
61 | self.Properties["highvalue"] = True
62 |
63 | self.Properties["functionallevel"] = functional_level
64 |
65 | self.Trusts = []
66 | self.Aces = []
67 | self.Links = []
68 | self.ChildObjects = []
69 | self.GPOChanges = {
70 | "AffectedComputers": [],
71 | "DcomUsers": [],
72 | "LocalAdmins": [],
73 | "PSRemoteUsers": [],
74 | "RemoteDesktopUsers": []
75 | }
76 | self.IsDeleted = False
77 | self.IsACLProtected = False
78 |
79 |
80 | def to_json(self, properties_level):
81 | self.Properties['isaclprotected'] = self.IsACLProtected
82 | domain = super().to_json(properties_level)
83 |
84 | domain["ObjectIdentifier"] = self.ObjectIdentifier
85 | domain["Trusts"] = self.Trusts
86 | domain["ContainedBy"] = None
87 | # The below is all unsupported as of now.
88 | domain["Aces"] = self.Aces
89 | domain["Links"] = self.Links
90 | domain["ChildObjects"] = self.ChildObjects
91 |
92 | self.GPOChanges["AffectedComputers"] = self.AffectedComputers
93 | self.GPOChanges["AffectedUsers"] = self.AffectedUsers
94 | domain["GPOChanges"] = self.GPOChanges
95 |
96 | domain["IsDeleted"] = self.IsDeleted
97 | domain["IsACLProtected"] = self.IsACLProtected
98 |
99 | return domain
100 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_domaintrust.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
2 | from bofhound.ad.models.bloodhound_object import BloodHoundObject
3 | from bloodhound.ad.utils import ADUtils
4 | from bloodhound.ad.trusts import ADDomainTrust
5 | from bofhound.ad.helpers import TrustType, TrustDirection
6 | from impacket.ldap.ldaptypes import LDAP_SID
7 | import logging
8 |
9 | class BloodHoundDomainTrust(object):
10 |
11 | def __init__(self, object):
12 | # Property for internal processing
13 | self.LocalDomainDn = ''
14 |
15 | # Property that holds final dict for domain JSON
16 | # {
17 | # "TargetDomainName": "",
18 | # "TargetDomainSid": "",
19 | # "IsTransitive": "",
20 | # "TrustDirection": "",
21 | # "TrustType": "",
22 | # "SidFilteringEnabled": ""
23 | # }
24 | self.TrustProperties = None
25 |
26 | if 'distinguishedname' in object.keys() and 'trustpartner' in object.keys() and \
27 | 'trustdirection' in object.keys() and 'trusttype' in object.keys() and 'trustattributes' in object.keys() and \
28 | 'securityidentifier' in object.keys():
29 |
30 | self.LocalDomainDn = BloodHoundObject.get_domain_component(object.get('distinguishedname')).upper()
31 | trust_partner = object.get('trustpartner').upper()
32 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
33 | logging.debug(f'Reading trust relationship between {ColorScheme.domain}{domain}[/] and {ColorScheme.domain}{trust_partner}[/]', extra=OBJ_EXTRA_FMT)
34 | domainsid = LDAP_SID()
35 | domainsid.fromCanonical(object.get('securityidentifier'))
36 | trust = ADDomainTrust(trust_partner, int(object.get('trustdirection')), object.get('trusttype'), int(object.get('trustattributes')), domainsid.getData())
37 | self.TrustProperties = trust.to_output()
38 |
39 | # BHCE now wants trusttype and direction defined as string names instead of int values
40 |
41 | self.TrustProperties['TrustDirection'] = TrustDirection(self.TrustProperties['TrustDirection']).name
42 | self.TrustProperties['TrustType'] = TrustType(self.TrustProperties['TrustType']).name
43 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_enterpriseca.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 | from bofhound.ad.helpers.cert_utils import PkiCertificateAuthorityFlags
6 |
7 |
8 | class BloodHoundEnterpriseCA(BloodHoundObject):
9 |
10 | GUI_PROPERTIES = [
11 | 'domain', 'name', 'distinguishedname', 'domainsid', 'isaclprotected',
12 | 'description', 'whencreated', 'flags', 'caname', 'dnshostname', 'certthumbprint',
13 | 'certname', 'certchain', 'hasbasicconstraints', 'basicconstraintpathlength',
14 | 'casecuritycollected', 'enrollmentagentrestrictionscollected', 'isuserspecifiessanenabledcollected',
15 | 'unresolvedpublishedtemplates'
16 | ]
17 |
18 | COMMON_PROPERTIES = [
19 | ]
20 |
21 | def __init__(self, object):
22 | super().__init__(object)
23 |
24 | self._entry_type = "EnterpriseCA"
25 | self.IsDeleted = False
26 | self.ContainedBy = {}
27 | self.IsACLProtected = False
28 | self.Properties['casecuritycollected'] = False
29 | self.Properties['enrollmentagentrestrictionscollected'] = False
30 | self.Properties['isuserspecifiessanenabledcollected'] = False
31 | self.Properties['unresolvedpublishedtemplates'] = []
32 | self.CARegistryData = None
33 | self.x509Certificate = None
34 |
35 | if 'objectguid' in object.keys():
36 | self.ObjectIdentifier = object.get("objectguid")
37 |
38 | if 'distinguishedname' in object.keys():
39 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
40 | self.Properties['domain'] = domain
41 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
42 |
43 | if 'description' in object.keys():
44 | self.Properties['description'] = object.get('description')
45 | else:
46 | self.Properties['description'] = None
47 |
48 | if 'flags' in object.keys():
49 | int_flag = int(object.get("flags"))
50 | self.Properties['flags'] = ', '.join([member.name for member in PkiCertificateAuthorityFlags if member.value & int_flag == member.value])
51 |
52 | if 'name' in object.keys():
53 | self.Properties['caname'] = object.get('name')
54 | if 'domain' in self.Properties.keys():
55 | self.Properties['name'] = object.get('name').upper() + "@" + self.Properties['domain'].upper()
56 |
57 | if 'dnshostname' in object.keys():
58 | self.Properties['dnshostname'] = object.get('dnshostname')
59 |
60 | if 'cacertificate' in object.keys():
61 | self.parse_cacertificate(object)
62 |
63 | if 'ntsecuritydescriptor' in object.keys():
64 | self.RawAces = object['ntsecuritydescriptor']
65 |
66 | self.HostingComputer = None
67 | self.EnabledCertTemplates = []
68 |
69 | if 'certificatetemplates' in object.keys():
70 | self.CertTemplates = object.get('certificatetemplates').split(', ')
71 |
72 |
73 | def to_json(self, properties_level):
74 | self.Properties['isaclprotected'] = self.IsACLProtected
75 | data = super().to_json(properties_level)
76 |
77 | data["HostingComputer"] = self.HostingComputer
78 | data["CARegistryData"] = self.CARegistryData
79 | data["EnabledCertTemplates"] = self.EnabledCertTemplates
80 | data["Aces"] = self.Aces
81 | data["ObjectIdentifier"] = self.ObjectIdentifier
82 | data["IsDeleted"] = self.IsDeleted
83 | data["IsACLProtected"] = self.IsACLProtected
84 | data["ContainedBy"] = self.ContainedBy
85 |
86 | return data
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_gpo.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundGPO(BloodHoundObject):
7 |
8 | GUI_PROPERTIES = [
9 | 'distinguishedname', 'whencreated',
10 | 'domain', 'domainsid', 'name', 'highvalue',
11 | 'description', 'gpcpath', 'isaclprotected'
12 | ]
13 |
14 | COMMON_PROPERTIES = [
15 | ]
16 |
17 | def __init__(self, object):
18 | super().__init__(object)
19 |
20 | self._entry_type = "GPO"
21 | self.ContainedBy = {}
22 |
23 | if 'distinguishedname' in object.keys() and 'displayname' in object.keys():
24 | self.Properties["domain"] = ADUtils.ldap2domain(object.get('distinguishedname').upper())
25 | self.Properties["name"] = f"{object.get('displayname').upper()}@{self.Properties['domain']}"
26 | logging.debug(f"Reading GPO object {ColorScheme.gpo}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
27 |
28 | if 'objectguid' in object.keys():
29 | self.ObjectIdentifier = object.get('objectguid').upper()
30 |
31 | if 'ntsecuritydescriptor' in object.keys():
32 | self.RawAces = object['ntsecuritydescriptor']
33 |
34 | if 'description' in object.keys():
35 | self.Properties["description"] = object.get('description')
36 |
37 | if 'gpcfilesyspath' in object.keys():
38 | self.Properties["gpcpath"] = object.get('gpcfilesyspath')
39 |
40 | self.Properties["highvalue"] = False
41 |
42 | self.Aces = []
43 |
44 | self.IsDeleted = False
45 | self.IsACLProtected = False
46 |
47 | def to_json(self, properties_level):
48 | self.Properties['isaclprotected'] = self.IsACLProtected
49 | gpo = super().to_json(properties_level)
50 |
51 | gpo["ObjectIdentifier"] = self.ObjectIdentifier
52 | gpo["ContainedBy"] = self.ContainedBy
53 | # The below is all unsupported as of now.
54 | gpo["Aces"] = self.Aces
55 | gpo["IsDeleted"] = self.IsDeleted
56 | gpo["IsACLProtected"] = self.IsACLProtected
57 |
58 | return gpo
59 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_group.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundGroup(BloodHoundObject):
7 |
8 | GUI_PROPERTIES = [
9 | 'distinguishedname', 'samaccountname', 'objectsid',
10 | 'admincount', 'description', 'whencreated',
11 | 'name', 'domain', 'domainsid'
12 | ]
13 |
14 | COMMON_PROPERTIES = [
15 | 'member', 'memberof'
16 | ]
17 |
18 | def __init__(self, object):
19 | super().__init__(object)
20 |
21 | self._entry_type = "Group"
22 | self.Members = []
23 | self.Aces = []
24 | self.ContainedBy = {}
25 | self.IsDeleted = False
26 | self.IsACLProtected = False
27 | self.MemberDNs = []
28 | self.MemberOfDNs = []
29 | self.IsACLProtected = False
30 |
31 | if 'distinguishedname' in object.keys() and 'samaccountname' in object.keys():
32 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
33 | name = f'{object.get("samaccountname")}@{domain}'.upper()
34 | self.Properties["name"] = name
35 | self.Properties["domain"] = domain
36 | logging.debug(f"Reading Group object {ColorScheme.group}{name}[/]", extra=OBJ_EXTRA_FMT)
37 |
38 | if 'objectsid' in object.keys():
39 | #objectid = BloodHoundObject.get_sid(object.get('objectsid', None), object.get('distinguishedname', None))
40 | objectid = object.get('objectsid')
41 | self.ObjectIdentifier = objectid
42 | self.Properties["domainsid"] = objectid.rsplit('-',1)[0]
43 |
44 |
45 | if 'distinguishedname' in object.keys():
46 | self.Properties["distinguishedname"] = object.get('distinguishedname', None).upper()
47 |
48 | if 'admincount' in object.keys():
49 | self.Properties["admincount"] = int(object.get('admincount')) == 1 # do not move this lower, it may break imports for users
50 | else:
51 | self.Properties["admincount"] = False
52 |
53 | if 'description' in object.keys():
54 | self.Properties["description"] = object.get('description')
55 |
56 | if 'member' in object.keys():
57 | self.MemberDNs = [f'CN={dn.upper()}' for dn in object.get('member').split(', CN=')]
58 | if len(self.MemberDNs) > 0:
59 | self.MemberDNs[0] = self.MemberDNs[0][3:]
60 |
61 | if 'ntsecuritydescriptor' in object.keys():
62 | self.RawAces = object['ntsecuritydescriptor']
63 |
64 | if 'memberof' in object.keys():
65 | self.MemberOfDNs = [f'CN={dn.upper()}' for dn in object.get('memberof').split(', CN=')]
66 | if len(self.MemberOfDNs) > 0:
67 | self.MemberOfDNs[0] = self.MemberOfDNs[0][3:]
68 |
69 |
70 | def add_group_member(self, object, object_type):
71 | member = {
72 | "ObjectIdentifier": object.ObjectIdentifier,
73 | "ObjectType": object_type
74 | }
75 | self.Members.append(member)
76 |
77 |
78 | def to_json(self, properties_level):
79 | group = super().to_json(properties_level)
80 | group["ObjectIdentifier"] = self.ObjectIdentifier
81 | group["ContainedBy"] = self.ContainedBy
82 | group["Aces"] = self.Aces
83 | group["Members"] = self.Members
84 | group["IsDeleted"] = self.IsDeleted
85 | group["IsACLProtected"] = self.IsACLProtected
86 |
87 | return group
88 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_issuancepolicy.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 | from asn1crypto import x509
6 | import hashlib
7 | import base64
8 |
9 |
10 | class BloodHoundIssuancePolicy(BloodHoundObject):
11 |
12 | GUI_PROPERTIES = [
13 | 'domain', 'name', 'distinguishedname', 'domainsid', 'isaclprotected',
14 | 'description', 'whencreated', 'displayname', 'certtemplateoid'
15 | ]
16 |
17 | COMMON_PROPERTIES = [
18 | ]
19 |
20 | def __init__(self, object):
21 | super().__init__(object)
22 |
23 | self._entry_type = "IssuancePolicy"
24 | self.IsDeleted = False
25 | self.ContainedBy = {}
26 | self.IsACLProtected = False
27 | self.GroupLink = None # {}
28 |
29 | if 'objectguid' in object.keys():
30 | self.ObjectIdentifier = object.get("objectguid")
31 |
32 | if 'distinguishedname' in object.keys():
33 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
34 | self.Properties['domain'] = domain
35 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
36 |
37 | # name relies on domain existing, so it can be appended to the end
38 | if 'displayname' in object.keys():
39 | self.Properties['name'] = f"{object.get('displayname').upper()}@{domain}"
40 |
41 | if 'displayname' in object.keys():
42 | self.Properties['displayname'] = object.get('displayname')
43 |
44 | if 'description' in object.keys():
45 | self.Properties['description'] = object.get('description')
46 | else:
47 | self.Properties['description'] = None
48 |
49 | if 'mspki-cert-template-oid' in object.keys():
50 | self.Properties['mspki-cert-template-oid'] = object.get('mspki-cert-template-oid')
51 |
52 | if 'ntsecuritydescriptor' in object.keys():
53 | self.RawAces = object['ntsecuritydescriptor']
54 |
55 |
56 | def to_json(self, properties_level):
57 | self.Properties['isaclprotected'] = self.IsACLProtected
58 | data = super().to_json(properties_level)
59 |
60 | data["Aces"] = self.Aces
61 | data["ObjectIdentifier"] = self.ObjectIdentifier
62 | data["IsDeleted"] = self.IsDeleted
63 | data["IsACLProtected"] = self.IsACLProtected
64 | data["ContainedBy"] = self.ContainedBy
65 |
66 | return data
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_ntauthstore.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 | from asn1crypto import x509
6 | import hashlib
7 | import base64
8 |
9 |
10 | class BloodHoundNTAuthStore(BloodHoundObject):
11 |
12 | GUI_PROPERTIES = [
13 | 'domain', 'name', 'distinguishedname', 'domainsid', 'isaclprotected',
14 | 'description', 'whencreated', 'certthumbprints'
15 | ]
16 |
17 | COMMON_PROPERTIES = [
18 | ]
19 |
20 | def __init__(self, object):
21 | super().__init__(object)
22 |
23 | self._entry_type = "NTAuthStore"
24 | self.IsDeleted = False
25 | self.ContainedBy = {}
26 | self.IsACLProtected = False
27 |
28 | self.Properties['certthumbprints'] = []
29 |
30 | if 'objectguid' in object.keys():
31 | self.ObjectIdentifier = object.get("objectguid")
32 |
33 | if 'distinguishedname' in object.keys():
34 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
35 | self.Properties['domain'] = domain
36 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
37 |
38 | # name relies on domain existing, so it can be appended to the end
39 | if 'name' in object.keys():
40 | self.Properties['name'] = f"{object.get('name').upper()}@{domain}"
41 |
42 | if 'description' in object.keys():
43 | self.Properties['description'] = object.get('description')
44 | else:
45 | self.Properties['description'] = None
46 |
47 | if 'cacertificate' in object.keys():
48 | certificate_b64 = object.get("cacertificate")
49 |
50 | certificate_b64_list = certificate_b64.split(", ")
51 | for cert in certificate_b64_list:
52 | certificate_byte_array = base64.b64decode(cert)
53 | thumbprint = hashlib.sha1(certificate_byte_array).hexdigest().upper()
54 | self.Properties['certthumbprints'].append(thumbprint)
55 |
56 | if 'ntsecuritydescriptor' in object.keys():
57 | self.RawAces = object['ntsecuritydescriptor']
58 |
59 |
60 | def to_json(self, properties_level):
61 | self.Properties['isaclprotected'] = self.IsACLProtected
62 | data = super().to_json(properties_level)
63 |
64 | data["Aces"] = self.Aces
65 | data["ObjectIdentifier"] = self.ObjectIdentifier
66 | data["IsDeleted"] = self.IsDeleted
67 | data["IsACLProtected"] = self.IsACLProtected
68 | data["ContainedBy"] = self.ContainedBy
69 |
70 | if "domainsid" in self.Properties:
71 | data["DomainSID"] = self.Properties["domainsid"]
72 |
73 | return data
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_object.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import calendar
3 | import hashlib
4 | import base64
5 | from asn1crypto import x509
6 | from datetime import datetime
7 | from bloodhound.enumeration.acls import SecurityDescriptor, ACL, ACCESS_ALLOWED_ACE, ACCESS_MASK, ACE, ACCESS_ALLOWED_OBJECT_ACE, has_extended_right, EXTRIGHTS_GUID_MAPPING, can_write_property, ace_applies
8 | from bloodhound.ad.utils import ADUtils
9 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
10 | from bofhound.ad.models.bloodhound_schema import BloodHoundSchema
11 | from bofhound.ad.helpers import PropertiesLevel
12 |
13 | # TODO: Move appropriate actions from this class to a super class of Users/Computers/maybe groups?
14 |
15 | class BloodHoundObject():
16 |
17 | GUI_PROPERTIES = [
18 | ]
19 |
20 | COMMON_PROPERTIES = [
21 | ]
22 |
23 | NEVER_SHOW_PROPERTIES = [
24 | 'ntsecuritydescriptor', 'serviceprincipalname'
25 | ]
26 |
27 | def __init__(self, object=None):
28 | self.ObjectIdentifier = None
29 | self.Aces = []
30 | self.RawAces = None
31 | self.Properties = {}
32 |
33 | if isinstance(object, dict):
34 | # Ensure all keys are lowercase
35 | for item in object.keys():
36 | self.Properties[item.lower()] = object[item]
37 |
38 | self.ObjectIdentifier = BloodHoundObject.get_sid(object.get('objectsid', None), object.get('distinguishedname', None))
39 |
40 | if 'distinguishedname' in object.keys():
41 | self.Properties["distinguishedname"] = object.get('distinguishedname', None).upper()
42 |
43 | self.__parse_whencreated(object)
44 |
45 |
46 | def get_primary_membership(self, object):
47 | """
48 | Construct primary membership from RID to SID (BloodHound 3.0 only)
49 | """
50 | try:
51 | primarygroupid = object.get('primarygroupid')
52 | return '%s-%s' % ('-'.join(object.get('objectsid').split('-')[:-1]), primarygroupid)
53 | except (TypeError, KeyError, AttributeError):
54 | # Doesn't have a primarygroupid, means it is probably a Group instead of a user
55 | return None
56 |
57 |
58 | def merge_entry(self, object, base_preference=False):
59 | """Merge the properties of another BloodHoundObject in with this one.
60 |
61 | Keyword arguments:
62 | object -- the new object to merge (required)
63 | base_preference -- whether or not to prefer the base object. If true, self's properties could be overwritten (default False)
64 | """
65 | self_attributes = self.__dict__.keys()
66 | for attr, value in object.__dict__.items():
67 | if attr == 'Properties':
68 | for k, v in getattr(object, attr).items():
69 | if not k in getattr(self, attr).keys():
70 | getattr(self, attr)[k] = v
71 | else:
72 | if k == 'distinguishedname':
73 | if not getattr(self, attr)[k]:
74 | getattr(self, attr)[k] = v
75 | if not base_preference:
76 | if getattr(object, attr).get(k, None):
77 | getattr(self, attr)[k] = v
78 |
79 | elif not attr in self_attributes:
80 | setattr(self, attr, value)
81 | else:
82 | if attr == 'ObjectIdentifier':
83 | if not self.ObjectIdentifier:
84 | setattr(self, attr, value)
85 |
86 | if not base_preference:
87 | if getattr(object, attr):
88 | setattr(self, attr, value)
89 |
90 |
91 |
92 | def get_distinguished_name(self):
93 | try:
94 | return self.Properties['distinguishedname'].upper()
95 | except KeyError:
96 | return None
97 |
98 |
99 | def get_property(self, property):
100 | try:
101 | return self.Properties[property]
102 | except KeyError:
103 | return None
104 |
105 |
106 | def to_json(self, properties_level):
107 | data = {
108 | "Properties": {}
109 | }
110 |
111 | match properties_level:
112 | case PropertiesLevel.Standard:
113 | for property in self.Properties.keys():
114 | if property in self.GUI_PROPERTIES \
115 | and property not in self.NEVER_SHOW_PROPERTIES:
116 | data["Properties"][property] = self.Properties[property]
117 | case PropertiesLevel.Member:
118 | for property in self.Properties.keys():
119 | if (property in self.COMMON_PROPERTIES or property in self.GUI_PROPERTIES) \
120 | and property not in self.NEVER_SHOW_PROPERTIES:
121 | data["Properties"][property] = self.Properties[property]
122 | case PropertiesLevel.All:
123 | data["Properties"] = self.Properties
124 |
125 | return data
126 |
127 |
128 | def __parse_whencreated(self, object):
129 | whencreated = object.get('whencreated', 0)
130 | try:
131 | if not isinstance(whencreated, int):
132 | whencreated = calendar.timegm(datetime.strptime(whencreated, "%Y%m%d%H%M%S.0Z").timetuple())
133 | self.Properties['whencreated'] = whencreated
134 | except:
135 | self.Properties['whencreated'] = whencreated
136 |
137 |
138 | # used by Domains and OUs
139 | def add_linked_gpo(self, object, gp_link_options):
140 | enforced = False
141 | if gp_link_options == '2':
142 | enforced = True
143 |
144 | link = {
145 | "GUID": object.ObjectIdentifier,
146 | "IsEnforced": False
147 | }
148 | self.Links.append(link)
149 |
150 |
151 | # used by Domains and OUs
152 | def add_ou_member(self, object, object_type):
153 | member = {
154 | "ObjectIdentifier": object.ObjectIdentifier,
155 | "ObjectType": object_type
156 | }
157 | self.ChildObjects.append(member)
158 |
159 |
160 | @staticmethod
161 | def get_sid(sid, dn=None):
162 | if sid in ADUtils.WELLKNOWN_SIDS:
163 | domain = ADUtils.ldap2domain(dn).upper()
164 | PrincipalSid = f'{domain}-{sid}'
165 | else:
166 | PrincipalSid = sid
167 |
168 | return PrincipalSid
169 |
170 |
171 | # Should probably move to ADDS?
172 | @staticmethod
173 | def get_domain_component(dn):
174 | dc = ''
175 | for component in dn.split(','):
176 | if component.startswith('DC='):
177 | dc += f"{component},"
178 | return dc[:-1]
179 |
180 |
181 | @staticmethod
182 | def get_dn(domain):
183 | components = domain.split('.')
184 | base = ''
185 | for comp in components:
186 | base += f',DC={comp}'
187 |
188 | return base[1:]
189 |
190 |
191 | @staticmethod
192 | def get_cn_from_dn(dn):
193 | for component in dn.split(',', 1):
194 | if component.startswith('CN='):
195 | return component[3:]
196 |
197 | #
198 | # for AIACAs, EnterpriseCAs, and RootCAs
199 | #
200 | def parse_cacertificate(self, object):
201 | certificate_b64 = object.get("cacertificate")
202 |
203 | certificate_byte_array = base64.b64decode(certificate_b64)
204 |
205 | #
206 | # thumbprint
207 | #
208 | thumbprint = hashlib.sha1(certificate_byte_array).hexdigest().upper()
209 | self.Properties['certthumbprint'] = thumbprint
210 |
211 | #
212 | # certname
213 | #
214 | certificate_byte_array = base64.b64decode(certificate_b64)
215 | ca_cert = x509.Certificate.load(certificate_byte_array)["tbs_certificate"]
216 | self.x509Certificate = ca_cert # set for post-processing
217 | self.Properties['certname'] = ca_cert['subject'].native.get('common_name', thumbprint)
218 |
219 | #
220 | # cert chain
221 | # not sure that Python libs offer a way to build the chain without access to the issuer cert like it seems SharpHound does
222 | # https://github.com/BloodHoundAD/SharpHoundCommon/blob/ea6b097927c5bb795adb8589e9a843293d36ae37/src/CommonLib/Processors/LDAPPropertyProcessor.cs#L772
223 | # so we will have the build the chain manually in post-processing
224 | #
225 | self.Properties['certchain'] = []
226 |
227 | #
228 | # extensions (hasbasicconstraints, basicconstraintpathlength)
229 | #
230 | self.Properties['hasbasicconstraints'] = False
231 | self.Properties['basicconstraintpathlength'] = 0
232 | for ext in ca_cert['extensions']:
233 | if ext['extn_id'].native == 'basic_constraints':
234 | basic_constraints = ext['extn_value'].parsed
235 | if basic_constraints['path_len_constraint'].native is not None:
236 | self.Properties['hasbasicconstraints'] = True
237 | self.Properties['basicconstraintpathlength'] = basic_constraints['path_len_constraint'].native
238 | break
239 |
240 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_ou.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundOU(BloodHoundObject):
7 |
8 | GUI_PROPERTIES = [
9 | 'distinguishedname', 'whencreated',
10 | 'domain', 'domainsid', 'name', 'highvalue', 'description',
11 | 'blocksinheritance', 'isaclprotected'
12 | ]
13 |
14 | COMMON_PROPERTIES = [
15 | ]
16 |
17 | def __init__(self, object):
18 | super().__init__(object)
19 |
20 | self._entry_type = "OU"
21 | self.GPLinks = []
22 | self.ContainedBy = {}
23 | self.Properties["blocksinheritance"] = False
24 |
25 | if 'distinguishedname' in object.keys() and 'ou' in object.keys():
26 | self.Properties["domain"] = ADUtils.ldap2domain(object.get('distinguishedname').upper())
27 | self.Properties["name"] = f"{object.get('ou').upper()}@{self.Properties['domain']}"
28 | logging.debug(f"Reading OU object {ColorScheme.ou}{self.Properties['name']}[/]", extra=OBJ_EXTRA_FMT)
29 |
30 | if 'objectguid' in object.keys():
31 | self.ObjectIdentifier = object.get('objectguid').upper()
32 |
33 | if 'ntsecuritydescriptor' in object.keys():
34 | self.RawAces = object['ntsecuritydescriptor']
35 |
36 | if 'description' in object.keys():
37 | self.Properties["description"] = object.get('description')
38 |
39 | if 'gplink' in object.keys():
40 | # [['DN1', 'GPLinkOptions1'], ['DN2', 'GPLinkOptions2'], ...]
41 | self.GPLinks = [link.upper()[:-1].split(';') for link in object.get('gplink').split('[LDAP//')][1:]
42 |
43 | if 'gpoptions' in object.keys():
44 | gpoptions = object.get('gpoptions')
45 | if gpoptions == '1':
46 | self.Properties["blocksinheritance"] = True
47 |
48 | self.Properties["highvalue"] = False
49 |
50 | self.Aces = []
51 | self.Links = []
52 | self.ChildObjects = []
53 | self.GPOChanges = {
54 | "AffectedComputers": [],
55 | "AffectedUsers": [],
56 | "DcomUsers": [],
57 | "LocalAdmins": [],
58 | "PSRemoteUsers": [],
59 | "RemoteDesktopUsers": []
60 | }
61 | self.IsDeleted = False
62 | self.IsACLProtected = False
63 |
64 |
65 | def to_json(self, properties_level):
66 | self.Properties['isaclprotected'] = self.IsACLProtected
67 | ou = super().to_json(properties_level)
68 |
69 | ou["ObjectIdentifier"] = self.ObjectIdentifier
70 | ou["ContainedBy"] = self.ContainedBy
71 | # The below is all unsupported as of now.
72 | ou["Aces"] = self.Aces
73 | ou["Links"] = self.Links
74 | ou["ChildObjects"] = self.ChildObjects
75 |
76 | self.GPOChanges["AffectedComputers"] = self.AffectedComputers
77 | self.GPOChanges["AffectedUsers"] = self.AffectedUsers
78 | ou["GPOChanges"] = self.GPOChanges
79 |
80 | ou["IsDeleted"] = self.IsDeleted
81 | ou["IsACLProtected"] = self.IsACLProtected
82 |
83 | return ou
84 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_rootca.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from .bloodhound_object import BloodHoundObject
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 |
7 | class BloodHoundRootCA(BloodHoundObject):
8 |
9 | GUI_PROPERTIES = [
10 | 'domain', 'name', 'distinguishedname', 'domainsid', 'isaclprotected',
11 | 'description', 'whencreated', 'certthumbprint', 'certname', 'certchain',
12 | 'hasbasicconstraints', 'basicconstraintpathlength'
13 | ]
14 |
15 | COMMON_PROPERTIES = [
16 | ]
17 |
18 | def __init__(self, object):
19 | super().__init__(object)
20 |
21 | self._entry_type = "RootCA"
22 | self.ContainedBy = {}
23 | self.IsACLProtected = False
24 | self.IsDeleted = False
25 | self.x509Certificate = None
26 |
27 | if 'objectguid' in object.keys():
28 | self.ObjectIdentifier = object.get("objectguid")
29 |
30 | if 'distinguishedname' in object.keys():
31 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
32 | self.Properties['domain'] = domain
33 | self.Properties['distinguishedname'] = object.get('distinguishedname').upper()
34 |
35 | if 'description' in object.keys():
36 | self.Properties['description'] = object.get('description')
37 | else:
38 | self.Properties['description'] = None
39 |
40 | if 'name' in object.keys():
41 | if 'domain' in self.Properties.keys():
42 | self.Properties['name'] = object.get('name').upper() + "@" + self.Properties['domain'].upper()
43 |
44 | if 'cacertificate' in object.keys():
45 | self.parse_cacertificate(object)
46 | # root CA certificates are self-signed
47 | self.Properties['certchain'] = [ self.Properties['certthumbprint'] ]
48 |
49 | if 'ntsecuritydescriptor' in object.keys():
50 | self.RawAces = object['ntsecuritydescriptor']
51 |
52 | def to_json(self, properties_level):
53 | self.Properties['isaclprotected'] = self.IsACLProtected
54 | data = super().to_json(properties_level)
55 | data['IsACLProtected'] = self.IsACLProtected
56 | data['IsDeleted'] = self.IsDeleted
57 | data["ObjectIdentifier"] = self.ObjectIdentifier
58 | data["ContainedBy"] = self.ContainedBy
59 | data["Aces"] = self.Aces
60 |
61 | if "domainsid" in self.Properties:
62 | data["DomainSID"] = self.Properties["domainsid"]
63 |
64 | return data
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_schema.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from uuid import UUID
3 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
4 | import logging
5 |
6 | class BloodHoundSchema(object):
7 |
8 | def __init__(self, object):
9 | self.Name = None
10 | self.SchemaIdGuid = None
11 |
12 | if 'name' in object.keys() and 'schemaidguid' in object.keys():
13 | self.Name = object.get('name').lower()
14 | try:
15 | value = object.get('schemaidguid')
16 | if '-' in value:
17 | self.SchemaIdGuid = value.lower()
18 | else:
19 | self.SchemaIdGuid = str(UUID(bytes_le=base64.b64decode(value))).lower()
20 | logging.debug(f"Reading Schema object {ColorScheme.schema}{self.Name}[/]", extra=OBJ_EXTRA_FMT)
21 | except:
22 | logging.warning(f"Error base64 decoding SchemaIDGUID attribute on Schema {ColorScheme.schema}{self.Name}[/]", extra=OBJ_EXTRA_FMT)
23 |
--------------------------------------------------------------------------------
/bofhound/ad/models/bloodhound_user.py:
--------------------------------------------------------------------------------
1 | from bloodhound.ad.utils import ADUtils
2 | from bloodhound.ad.structures import LDAP_SID
3 | from bloodhound.enumeration.memberships import MembershipEnumerator
4 | from .bloodhound_object import BloodHoundObject
5 | from bofhound.logger import OBJ_EXTRA_FMT, ColorScheme
6 | import logging
7 |
8 | class BloodHoundUser(BloodHoundObject):
9 |
10 | GUI_PROPERTIES = [
11 | 'domain', 'name', 'distinguishedname', 'domainsid', 'samaccountname',
12 | 'isaclprotected', 'description', 'whencreated', 'sensitive',
13 | 'dontreqpreauth', 'passwordnotreqd', 'unconstraineddelegation',
14 | 'pwdneverexpires', 'enabled', 'trustedtoauth', 'lastlogon', 'lastlogontimestamp',
15 | 'pwdlastset', 'serviceprincipalnames', 'hasspn', 'admincount',
16 | 'displayname', 'email', 'title', 'homedirectory', 'userpassword',
17 | 'unixpassword', 'unicodepassword', 'sfupassword', 'logonscript', 'sidhistory'
18 | ]
19 |
20 | COMMON_PROPERTIES = [
21 | 'samaccounttype', 'objectsid', 'primarygroupid', 'isdeleted',
22 | 'msds-groupmsamembership', 'serviceprincipalname', 'useraccountcontrol',
23 | 'mail', 'msds-allowedtodelegateto', 'unicodepwd', 'allowedtodelegate',
24 | 'memberof'
25 | ]
26 |
27 | def __init__(self, object=None):
28 | super().__init__(object)
29 |
30 | self._entry_type = "User"
31 | self.PrimaryGroupSid = None
32 | self.AllowedToDelegate = []
33 | self.Aces = []
34 | self.ContainedBy = {}
35 | self.SPNTargets = []
36 | self.HasSIDHistory = []
37 | self.IsACLProtected = False
38 | self.MemberOfDNs = []
39 |
40 | if isinstance(object, dict):
41 | self.PrimaryGroupSid = self.get_primary_membership(object) # Returns none if not exist
42 |
43 | if 'distinguishedname' in object.keys() and 'samaccountname' in object.keys():
44 | domain = ADUtils.ldap2domain(object.get('distinguishedname')).upper()
45 | name = f'{object.get("samaccountname")}@{domain}'.upper()
46 | self.Properties["name"] = name
47 | self.Properties["domain"] = domain
48 | logging.debug(f"Reading User object {ColorScheme.user}{name}[/]", extra=OBJ_EXTRA_FMT)
49 |
50 | if 'admincount' in object.keys():
51 | self.Properties["admincount"] = int(object.get('admincount')) == 1 # do not move this lower, it may break imports for users
52 |
53 | # self.Properties["highvalue"] = False,
54 |
55 | if 'useraccountcontrol' in object.keys():
56 | uac = int(object.get('useraccountcontrol', 0))
57 | self.Properties["unconstraineddelegation"] = uac & 0x00080000 == 0x00080000
58 | self.Properties["passwordnotreqd"] = uac & 0x00000020 == 0x00000020
59 | self.Properties["enabled"] = uac & 2 == 0
60 | self.Properties["dontreqpreauth"] = uac & 0x00400000 == 0x00400000
61 | self.Properties["sensitive"] = uac & 0x00100000 == 0x00100000
62 | self.Properties["trustedtoauth"] = uac & 0x01000000 == 0x01000000
63 | self.Properties["pwdneverexpires"] = uac & 0x00010000 == 0x00010000
64 |
65 | if 'lastlogon' in object.keys():
66 | self.Properties["lastlogon"] = ADUtils.win_timestamp_to_unix(
67 | int(object.get('lastlogon'))
68 | )
69 |
70 | if 'lastlogontimestamp' in object.keys():
71 | self.Properties["lastlogontimestamp"] = ADUtils.win_timestamp_to_unix(
72 | int(object.get('lastlogontimestamp'))
73 | )
74 |
75 | if 'pwdlastset' in object.keys():
76 | self.Properties["pwdlastset"] = ADUtils.win_timestamp_to_unix(
77 | int(object.get('pwdlastset'))
78 | )
79 |
80 | if 'useraccountcontrol' in object.keys():
81 | self.Properties["dontreqpreauth"] = int(object.get('useraccountcontrol', 0)) & 0x00400000 == 0x00400000
82 | self.Properties["pwdneverexpires"] = int(object.get('useraccountcontrol', 0)) & 0x00010000 == 0x00010000
83 | self.Properties["sensitive"] = int(object.get('useraccountcontrol', 0)) & 0x00100000 == 0x00100000
84 |
85 | if 'serviceprincipalname' in object.keys():
86 | self.Properties["serviceprincipalnames"] = object.get('serviceprincipalname').split(',')
87 | self.Properties['hasspn'] = True
88 | else:
89 | self.Properties["serviceprincipalnames"] = []
90 | self.Properties['hasspn'] = False
91 |
92 | if 'serviceprincipalname' in object.keys():
93 | self.Properties["hasspn"] = len(object.get('serviceprincipalname', [])) > 0
94 |
95 | if 'samaccounttype' in object.keys():
96 | self.Properties["samaccounttype"] = object.get('samaccounttype')
97 |
98 | if 'displayname' in object.keys():
99 | self.Properties["displayname"] = object.get('displayname')
100 |
101 | if 'mail' in object.keys():
102 | self.Properties["email"] = object.get('mail')
103 |
104 | if 'title' in object.keys():
105 | self.Properties["title"] = object.get('title')
106 |
107 | if 'homedirectory' in object.keys():
108 | self.Properties["homedirectory"] = object.get('homedirectory')
109 |
110 | if 'description' in object.keys():
111 | self.Properties["description"] = object.get('description')
112 |
113 | if 'userpassword' in object.keys():
114 | self.Properties["userpassword"] = ADUtils.ensure_string(object.get('userpassword'))
115 |
116 | if 'sidhistory' in object.keys():
117 | self.Properties["sidhistory"] = [LDAP_SID(bsid).formatCanonical() for bsid in object.get('sIDHistory', [])]
118 |
119 | if 'msds-allowedtodelegateto' in object.keys():
120 | if len(object.get('msds-allowedtodelegateto', [])) > 0:
121 | self.Properties['allowedtodelegate'] = object.get('msds-allowedtodelegateto', [])
122 |
123 | if 'ntsecuritydescriptor' in object.keys():
124 | self.RawAces = object['ntsecuritydescriptor']
125 |
126 | if 'memberof' in object.keys():
127 | self.MemberOfDNs = [f'CN={dn.upper()}' for dn in object.get('memberof').split(', CN=')]
128 | if len(self.MemberOfDNs) > 0:
129 | self.MemberOfDNs[0] = self.MemberOfDNs[0][3:]
130 |
131 | ### TODO Not supported for the moment
132 | # self.Properties['displayname'] = None
133 | # self.Properties['email'] = None
134 | # self.Properties['title'] = None
135 | # self.Properties['homedirectory'] = None
136 | # self.Properties['userpassword'] = None
137 | # self.Properties['unixpassword'] = None
138 | # self.Properties['unicodepassword'] = None
139 | # self.Properties['sfupassword'] = None
140 | # self.Properties['logonscript'] = None
141 | # self.Properties['sidhistory'] = []
142 |
143 |
144 | def to_json(self, properties_level):
145 | self.Properties['isaclprotected'] = self.IsACLProtected
146 | user = super().to_json(properties_level)
147 |
148 | user["ObjectIdentifier"] = self.ObjectIdentifier
149 | user["ContainedBy"] = self.ContainedBy
150 | user["AllowedToDelegate"] = self.AllowedToDelegate
151 | user["PrimaryGroupSID"] = self.PrimaryGroupSid
152 |
153 | user["Aces"] = self.Aces
154 | user["SPNTargets"] = self.SPNTargets
155 | user["HasSIDHistory"] = self.HasSIDHistory
156 | user["IsACLProtected"] = self.IsACLProtected
157 |
158 | # TODO: RBCD
159 | # Process resource-based constrained delegation
160 | # _, aces = parse_binary_acl(data,
161 | # 'computer',
162 | # object.get('msDS-AllowedToActOnBehalfOfOtherIdentity'),
163 | # self.addc.objecttype_guid_map)
164 | # outdata = self.aceresolver.resolve_aces(aces)
165 | # for delegated in outdata:
166 | # if delegated['RightName'] == 'Owner':
167 | # continue
168 | # if delegated['RightName'] == 'GenericAll':
169 | # data['AllowedToAct'].append({'MemberId': delegated['PrincipalSID'], 'MemberType': delegated['PrincipalType']})
170 | #
171 | # # Run ACL collection if this was not already done centrally
172 | # if 'acl' in collect and not skip_acl:
173 | # _, aces = parse_binary_acl(data,
174 | # 'computer',
175 | # object.get('nTSecurityDescriptor',
176 | # raw=True),
177 | # self.addc.objecttype_guid_map)
178 | # # Parse aces
179 | # data['Aces'] = self.aceresolver.resolve_aces(aces)
180 |
181 | return user
182 |
--------------------------------------------------------------------------------
/bofhound/local/__init__.py:
--------------------------------------------------------------------------------
1 | from .localbroker import LocalBroker
--------------------------------------------------------------------------------
/bofhound/local/localbroker.py:
--------------------------------------------------------------------------------
1 | from .models import LocalGroupMembership, LocalPrivilegedSession, LocalSession, LocalRegistrySession
2 | from bofhound.parsers.shared_parsers import NetSessionBofParser, NetLoggedOnBofParser, NetLocalGroupBofParser, RegSessionBofParser
3 |
4 |
5 | class LocalBroker:
6 |
7 | def __init__(self):
8 | self.privileged_sessions = set()
9 | self.sessions = set()
10 | self.local_group_memberships = set()
11 | self.registry_sessions = set()
12 |
13 |
14 | # take in known domain sids so we can filter out local accounts
15 | # and accounts with unknown domains
16 | def import_objects(self, objects, known_domain_sids):
17 |
18 | for object in objects:
19 |
20 | if object["ObjectType"] == NetLoggedOnBofParser.OBJECT_TYPE:
21 | priv_session = LocalPrivilegedSession(object)
22 | if priv_session.should_import():
23 | self.privileged_sessions.add(priv_session)
24 |
25 | elif object["ObjectType"] == NetSessionBofParser.OBJECT_TYPE:
26 | session = LocalSession(object)
27 | if session.should_import():
28 | self.sessions.add(session)
29 |
30 | elif object["ObjectType"] == NetLocalGroupBofParser.OBJECT_TYPE:
31 | local_group_membership = LocalGroupMembership(object)
32 | if local_group_membership.should_import(known_domain_sids):
33 | self.local_group_memberships.add(local_group_membership)
34 |
35 | elif object["ObjectType"] == RegSessionBofParser.OBJECT_TYPE:
36 | registry_session = LocalRegistrySession(object)
37 | if registry_session.should_import(known_domain_sids):
38 | self.registry_sessions.add(registry_session)
39 |
--------------------------------------------------------------------------------
/bofhound/local/models/__init__.py:
--------------------------------------------------------------------------------
1 | from .local_groupmembership import LocalGroupMembership
2 | from .local_privilegedsession import LocalPrivilegedSession
3 | from .local_session import LocalSession
4 | from .local_registrysession import LocalRegistrySession
--------------------------------------------------------------------------------
/bofhound/local/models/local_groupmembership.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import ColorScheme, OBJ_EXTRA_FMT
2 | import logging
3 | import ipaddress
4 |
5 |
6 | class LocalGroupMembership:
7 | LOCALGROUP_HOST = "Host"
8 | LOCALGROUP_GROUP = "Group"
9 | LOCALGROUP_MEMBER = "Member"
10 | LOCALGROUP_MEMBER_SID = "MemberSid"
11 | LOCALGROUP_MEMBER_SID_TYPE = "MemberSidType"
12 | LOCALGROUP_NAMES = [
13 | "administrators",
14 | "remote desktop users",
15 | "remote management users",
16 | "distributed com users"
17 | ]
18 |
19 | def __init__(self, object):
20 | self.host_name = None
21 | self.host_domain = None
22 | self.host_fqdn = None
23 | self.group = None
24 | self.member = None
25 | self.member_netbios_domain = None
26 | self.member_sid = None
27 | self.member_sid_type = None
28 |
29 | # will be set to True if correlated to a computer object
30 | self.matched = False
31 |
32 | try:
33 | ipaddress.ip_address(object[LocalGroupMembership.LOCALGROUP_HOST])
34 | logging.debug(f"Skipping local group member on {object[LocalGroupMembership.LOCALGROUP_HOST]} due to IP instead of hostname")
35 | return
36 | except:
37 | pass
38 |
39 | if LocalGroupMembership.LOCALGROUP_GROUP in object.keys():
40 | self.group = object[LocalGroupMembership.LOCALGROUP_GROUP]
41 |
42 | if LocalGroupMembership.LOCALGROUP_MEMBER in object.keys():
43 | parts = object[LocalGroupMembership.LOCALGROUP_MEMBER].split('\\')
44 | if len(parts) == 2:
45 | self.member = parts[1]
46 | self.member_netbios_domain = parts[0]
47 | else:
48 | self.member = parts[0]
49 |
50 | if LocalGroupMembership.LOCALGROUP_HOST in object.keys():
51 | if '.' in object[LocalGroupMembership.LOCALGROUP_HOST]:
52 | self.host_name = object[LocalGroupMembership.LOCALGROUP_HOST].split('.')[0]
53 | self.host_domain = '.'.join(object[LocalGroupMembership.LOCALGROUP_HOST].split('.')[1:])
54 | self.host_fqdn = object[LocalGroupMembership.LOCALGROUP_HOST]
55 | else:
56 | self.host_name = object[LocalGroupMembership.LOCALGROUP_HOST]
57 | logging.debug(f"FQDN missing from hostname for {ColorScheme.user}{self.user}[/] session on {ColorScheme.computer}{self.host_name}[/]", extra=OBJ_EXTRA_FMT)
58 |
59 | if LocalGroupMembership.LOCALGROUP_MEMBER_SID in object.keys():
60 | self.member_sid = object[LocalGroupMembership.LOCALGROUP_MEMBER_SID]
61 |
62 | if LocalGroupMembership.LOCALGROUP_MEMBER_SID_TYPE in object.keys():
63 | self.member_sid_type = object[LocalGroupMembership.LOCALGROUP_MEMBER_SID_TYPE]
64 |
65 |
66 | def should_import(self, known_domain_sids):
67 | # missing required attributes
68 | if self.host_name is None or self.group is None \
69 | or self.member_sid is None or self.member_sid_type is None:
70 | return False
71 |
72 | # filter out local groups we don't care about
73 | if self.group.lower() not in LocalGroupMembership.LOCALGROUP_NAMES:
74 | return False
75 |
76 | # do not import local account sessions or
77 | # user sessions from unknown domains
78 | if self.member_sid.rsplit('-', 1)[0] not in known_domain_sids:
79 | color = ColorScheme.user if self.member_sid_type == "User" else ColorScheme.group
80 | logging.debug(f"Skipping local group membership for {color}{self.member}[/] since domain SID is unfamiliar", extra=OBJ_EXTRA_FMT)
81 | return False
82 |
83 | computer = f"{self.host_name}.{self.host_domain}" if self.host_domain else self.host_name
84 | user = f"{self.member}@{self.member_netbios_domain}" if self.member_netbios_domain else self.member
85 | logging.debug(f"Local group member found for {ColorScheme.user}{user}[/] on {ColorScheme.computer}{computer}[/]", extra=OBJ_EXTRA_FMT)
86 | return True
87 |
88 |
89 | # so that a set can be used to keep a unique list of objects
90 | def __eq__(self, other):
91 | return (self.host_name, self.host_domain, self.group, self.member_sid) == \
92 | (other.host_name, other.host_domain, other.group, other.member_sid)
93 |
94 |
95 | # so that a set can be used to keep a unique list of objects
96 | def __hash__(self):
97 | return hash((self.host_name, self.host_domain, self.group, self.member_sid))
98 |
99 |
100 | # for debugging
101 | def __repr__(self):
102 | return f"LocalGroupMembership(host_name={self.host_name}, group={self.group}, member={self.member}, member_netbios_domain={self.member_netbios_domain}, member_sid={self.member_sid}, member_sid_type={self.member_sid_type})"
103 |
104 |
--------------------------------------------------------------------------------
/bofhound/local/models/local_privilegedsession.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import ColorScheme, OBJ_EXTRA_FMT
2 | import logging
3 | import ipaddress
4 |
5 |
6 | class LocalPrivilegedSession:
7 | PS_HOST = "Host"
8 | PS_USERNAME = "Username"
9 | PS_DOMAIN = "Domain"
10 |
11 | def __init__(self, object):
12 | self.host_name = None
13 | self.host_domain = None
14 | self.host_fqdn = None
15 | self.user = None
16 | self.user_domain = None
17 |
18 | # will be set to True if correlated to a computer object
19 | self.matched = False
20 |
21 | try:
22 | ipaddress.ip_address(object[LocalPrivilegedSession.PS_HOST])
23 | logging.debug(f"Skipping session on {object[LocalPrivilegedSession.PS_HOST]} due to IP instead of hostname")
24 | return
25 | except:
26 | pass
27 |
28 | if LocalPrivilegedSession.PS_USERNAME in object.keys():
29 | self.user = object[LocalPrivilegedSession.PS_USERNAME]
30 |
31 | if LocalPrivilegedSession.PS_DOMAIN in object.keys():
32 | self.user_domain = object[LocalPrivilegedSession.PS_DOMAIN]
33 |
34 | if LocalPrivilegedSession.PS_HOST in object.keys():
35 | self.host_fqdn = object[LocalPrivilegedSession.PS_HOST]
36 | if '.' in self.host_fqdn:
37 | self.host_name = self.host_fqdn.split('.')[0]
38 | self.host_domain = '.'.join(self.host_fqdn.split('.')[1:])
39 | else:
40 | logging.debug(f"FQDN missing from hostname for {ColorScheme.user}{self.user}[/] session on {ColorScheme.computer}{self.host_fqdn}[/]", extra=OBJ_EXTRA_FMT)
41 |
42 | def should_import(self):
43 | # missing required attributes
44 | if self.host_name is None or self.host_domain is None \
45 | or self.user is None or self.user_domain is None:
46 | return False
47 |
48 | # do not import computer accounts
49 | if self.user.endswith('$'):
50 | return False
51 |
52 | # do not import local accounts
53 | if self.host_name.lower() == self.user_domain.lower():
54 | return False
55 |
56 | logging.debug(f"NetWkstaUserEnum session found for {ColorScheme.user}{self.user}@{self.user_domain}[/] on {ColorScheme.computer}{self.host_fqdn}[/]", extra=OBJ_EXTRA_FMT)
57 | return True
58 |
59 |
60 | # so that a set can be used to keep a unique list of objects
61 | def __eq__(self, other):
62 | return (self.host_name, self.host_domain, self.host_fqdn, self.user, self.user_domain) == \
63 | (other.host_name, other.host_domain, other.host_fqdn, other.user, other.user_domain)
64 |
65 |
66 | # so that a set can be used to keep a unique list of objects
67 | def __hash__(self):
68 | return hash((self.host_name, self.host_domain, self.host_fqdn, self.user, self.user_domain))
69 |
70 |
71 | # for debugging
72 | def __repr__(self):
73 | return f"LocalPrivilegedSession(host_name={self.host_name}, host_domain={self.host_domain}, host_fqdn={self.host_fqdn}, user={self.user}, user_domain={self.user_domain})"
--------------------------------------------------------------------------------
/bofhound/local/models/local_registrysession.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import ColorScheme, OBJ_EXTRA_FMT
2 | import logging
3 | import ipaddress
4 |
5 |
6 | class LocalRegistrySession:
7 | REGSESSION_HOST = "Host"
8 | REGSESSION_USER_SID = "UserSid"
9 |
10 |
11 | def __init__(self, object):
12 | self.user_sid = None
13 | self.host_name = None
14 | self.host_domain = None
15 | self.host_fqdn = None
16 |
17 | try:
18 | ipaddress.ip_address(object[LocalRegistrySession.REGSESSION_HOST])
19 | logging.debug(f"Skipping session on {object[LocalRegistrySession.REGSESSION_HOST]} due to IP instead of hostname")
20 | return
21 | except:
22 | pass
23 |
24 | # will be set to True if correlated to a computer object
25 | self.matched = False
26 |
27 | if LocalRegistrySession.REGSESSION_USER_SID in object.keys():
28 | self.user_sid = object[LocalRegistrySession.REGSESSION_USER_SID]
29 |
30 | if LocalRegistrySession.REGSESSION_HOST in object.keys():
31 | if '.' in object[LocalRegistrySession.REGSESSION_HOST]:
32 | self.host_fqdn = object[LocalRegistrySession.REGSESSION_HOST]
33 | self.host_name = self.host_fqdn.split('.')[0]
34 | self.host_domain = '.'.join(self.host_fqdn.split('.')[1:])
35 | else:
36 | self.host_name = object[LocalRegistrySession.REGSESSION_HOST]
37 | logging.debug(f"FQDN missing from hostname for {ColorScheme.user}{self.user_sid}[/] session on {ColorScheme.computer}{self.host_name}[/]", extra=OBJ_EXTRA_FMT)
38 |
39 | def should_import(self, known_domain_sids):
40 | # missing required attributes
41 | if self.user_sid is None or self.host_name is None:
42 | return False
43 |
44 | # do not import local account sessions or
45 | # user sessions from unknown domains
46 | if self.user_sid.rsplit('-', 1)[0] not in known_domain_sids:
47 | logging.debug(f"Skipping session for {ColorScheme.user}{self.user_sid}[/] since domain SID is unfamiliar", extra=OBJ_EXTRA_FMT)
48 | return False
49 |
50 | computer = self.host_fqdn if self.host_fqdn else self.host_name
51 | logging.debug(f"Registry session found for {ColorScheme.user}{self.user_sid}[/] on {ColorScheme.computer}{computer}[/]", extra=OBJ_EXTRA_FMT)
52 | return True
53 |
54 |
55 | # so that a set can be used to keep a unique list of objects
56 | def __eq__(self, other):
57 | return (self.user_sid, self.host_name, self.host_domain) == \
58 | (other.user_sid, other.host_name, other.host_domain)
59 |
60 |
61 | # so that a set can be used to keep a unique list of objects
62 | def __hash__(self):
63 | return hash((self.user_sid, self.host_name, self.host_domain))
64 |
65 |
66 | # for debugging
67 | def __repr__(self):
68 | return f"RegistrySession"
--------------------------------------------------------------------------------
/bofhound/local/models/local_session.py:
--------------------------------------------------------------------------------
1 | from bofhound.logger import ColorScheme, OBJ_EXTRA_FMT
2 | import logging
3 |
4 |
5 | class LocalSession:
6 | SESSION_PTR = "PTR"
7 | SESSION_USER = "User"
8 | SESSION_COMPUTER_NAME = "ComputerName"
9 | SESSION_COMPUTER_DOMAIN = "ComputerDomain"
10 |
11 | def __init__(self, object):
12 | self.username = None
13 | self.ptr_record = None
14 | self.computer_name = None
15 | self.computer_netbios_domain = None
16 | self.computer_domain = None
17 |
18 | # will be set to True if correlated to a computer object
19 | self.matched = False
20 |
21 | if LocalSession.SESSION_PTR in object.keys():
22 | if "reverse lookup failed" not in object[LocalSession.SESSION_PTR]:
23 | self.ptr_record = object[LocalSession.SESSION_PTR]
24 | self.computer_name = self.ptr_record.split('.')[0]
25 | self.computer_domain = '.'.join(self.ptr_record.split('.')[1:])
26 |
27 | if LocalSession.SESSION_USER in object.keys():
28 | self.username = object[LocalSession.SESSION_USER]
29 |
30 | if LocalSession.SESSION_COMPUTER_NAME in object.keys():
31 | self.computer_name = object[LocalSession.SESSION_COMPUTER_NAME]
32 |
33 | if LocalSession.SESSION_COMPUTER_DOMAIN in object.keys():
34 | self.computer_netbios_domain = object[LocalSession.SESSION_COMPUTER_DOMAIN]
35 |
36 |
37 | def should_import(self):
38 | # missing required attributes
39 | if self.username is None or self.ptr_record is None \
40 | and self.computer_name is None or (self.computer_domain is None and self.computer_netbios_domain is None):
41 | return False
42 |
43 | # do not import sessions if NetWkstaGetInfo failed
44 | fail = "NetWkstaGetInfo Failed;"
45 | if self.computer_name is not None and self.computer_netbios_domain is not None:
46 | if self.computer_name.startswith(fail) or self.computer_netbios_domain.startswith(fail):
47 | return False
48 |
49 | # do not import computer accounts
50 | if self.username.endswith('$'):
51 | return False
52 |
53 | # do not import anonymous sessions
54 | if self.username.upper() == 'ANONYMOUS LOGON':
55 | return False
56 |
57 | computer = self.ptr_record if self.ptr_record else self.computer_name
58 | logging.debug(f"NetSessionEnum session found for {ColorScheme.user}{self.username}[/] on {ColorScheme.computer}{computer}[/]", extra=OBJ_EXTRA_FMT)
59 | return True
60 |
61 |
62 | # so that a set can be used to keep a unique list of objects
63 | def __eq__(self, other):
64 | return (self.username, self.ptr_record, self.computer_name, self.computer_domain) == \
65 | (other.username, other.ptr_record, other.computer_name, other.computer_domain)
66 |
67 |
68 | # so that a set can be used to keep a unique list of objects
69 | def __hash__(self):
70 | return hash((self.username, self.ptr_record, self.computer_name, self.computer_domain))
71 |
72 | # for debugging
73 | def __repr__(self):
74 | return f"LocalSession(username={self.username}, ptr_record={self.ptr_record}, computer_name={self.computer_name}, computer_domain={self.computer_domain}, computer_netbios_domain={self.computer_netbios_domain})"
--------------------------------------------------------------------------------
/bofhound/logger.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from rich.logging import RichHandler
3 |
4 | class ColorScheme:
5 | domain = "[green]"
6 | user = "[sea_green3]"
7 | computer = "[red1]"
8 | group = "[gold1]"
9 | enterpriseca = "[medium_purple1]"
10 | aiaca = "[medium_purple2]"
11 | rootca = "[medium_purple3]"
12 | certtemplate = "[bright_magenta]"
13 | schema = "[deep_sky_blue1]"
14 | ou = "[dark_orange]"
15 | containers = "[orange]"
16 | gpo = "[purple]"
17 |
18 | OBJ_EXTRA_FMT = {
19 | "markup": True,
20 | "highlighter": False
21 | }
22 |
23 | FORMAT = "%(message)s"
24 | logging.basicConfig(
25 | level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler(omit_repeated_times=False, show_path=False, keywords=[])]
26 | )
27 |
28 | #logging.getLogger("rich")
--------------------------------------------------------------------------------
/bofhound/parsers/__init__.py:
--------------------------------------------------------------------------------
1 | from .ldap_search_bof import LdapSearchBofParser
2 | from .brc4_ldap_sentinel import Brc4LdapSentinelParser
3 | from .havoc import HavocParser
4 | from .parsertype import ParserType
5 | from .outflankc2 import OutflankC2JsonParser
--------------------------------------------------------------------------------
/bofhound/parsers/brc4_ldap_sentinel.py:
--------------------------------------------------------------------------------
1 | import re
2 | import codecs
3 | import logging
4 | from datetime import datetime as dt
5 | from bofhound.parsers import LdapSearchBofParser
6 |
7 | class Brc4LdapSentinelParser(LdapSearchBofParser):
8 | # BRC4 LDAP Sentinel currently only queries attributes=["*"] and objectClass
9 | # is always the top result. May need to be updated in the future.
10 | START_BOUNDARY = '[+] objectclass :'
11 | END_BOUNDARY = '+-------------------------------------------------------------------+'
12 |
13 | FORMATTED_TS_ATTRS = ['lastlogontimestamp', 'lastlogon', 'lastlogoff', 'pwdlastset', 'accountexpires']
14 | ISO_8601_TS_ATTRS = ['dscorepropagationdata', 'whenchanged', 'whencreated']
15 | BRACKETED_ATTRS = ['objectguid']
16 | SEMICOLON_DELIMITED_ATTRS = ['serviceprincipalname', 'memberof', 'member', 'objectclass']
17 |
18 | def __init__(self):
19 | pass #self.objects = []
20 |
21 | #
22 | # Legacy, used by test cases for 1 liner
23 | # Removed from __main__.py to avoid duplicating file reads and formatting
24 | #
25 | @staticmethod
26 | def parse_file(file):
27 | with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
28 | return Brc4LdapSentinelParser.parse_data(f.read())
29 |
30 |
31 | #
32 | # Replaces parse_file() usage in __main__.py to avoid duplicate file reads
33 | #
34 | @staticmethod
35 | def prep_file(file):
36 | with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
37 | return f.read()
38 |
39 |
40 | @staticmethod
41 | def parse_data(contents):
42 | parsed_objects = []
43 | current_object = None
44 | in_result_region = False
45 |
46 | in_result_region = False
47 |
48 | lines = contents.splitlines()
49 | for line in lines:
50 |
51 | if len(line) == 0:
52 | continue
53 |
54 | is_start_boundary_line = Brc4LdapSentinelParser._is_start_boundary_line(line)
55 | is_end_boundary_line = Brc4LdapSentinelParser._is_end_boundary_line(line)
56 |
57 | if not in_result_region and not is_start_boundary_line:
58 | continue
59 |
60 | if is_start_boundary_line:
61 | if not in_result_region:
62 | in_result_region = True
63 |
64 | current_object = {}
65 |
66 | elif is_end_boundary_line:
67 | parsed_objects.append(current_object)
68 | in_result_region = False
69 | current_object = None
70 | continue
71 |
72 | data = line.split(': ')
73 |
74 | try:
75 | data = line.split(':', 1)
76 | attr = data[0].replace('[+]', '').strip().lower()
77 | value = data[1].strip()
78 |
79 | # BRc4 formats some timestamps for us that we need to revert to raw values
80 | if attr in Brc4LdapSentinelParser.FORMATTED_TS_ATTRS:
81 | if value.lower() in ['never expires', 'value not set']:
82 | continue
83 | timestamp_obj = dt.strptime(value, '%m/%d/%Y %I:%M:%S %p')
84 | value = int((timestamp_obj - dt(1601, 1, 1)).total_seconds() * 10000000)
85 |
86 | if attr in Brc4LdapSentinelParser.ISO_8601_TS_ATTRS:
87 | formatted_ts = []
88 | for ts in value.split(';'):
89 | timestamp_obj = dt.strptime(ts.strip(), "%m/%d/%Y %I:%M:%S %p")
90 | timestamp_str = timestamp_obj.strftime("%Y%m%d%H%M%S.0Z")
91 | formatted_ts.append(timestamp_str)
92 | value = ', '.join(formatted_ts)
93 |
94 | # BRc4 formats some attributes with surroudning {} we need to remove
95 | if attr in Brc4LdapSentinelParser.BRACKETED_ATTRS:
96 | value = value[1:-1]
97 |
98 | # BRc4 delimits some list-esque attributes with semicolons
99 | # when our BH models expect commas
100 | if attr in Brc4LdapSentinelParser.SEMICOLON_DELIMITED_ATTRS:
101 | value = value.replace('; ', ', ')
102 |
103 | # BRc4 puts the trustDirection attribute within securityidentifier
104 | if attr == 'securityidentifier' and 'trustdirection' in value.lower():
105 | trust_direction = value.lower().split('trustdirection ')[1]
106 | current_object['trustdirection'] = trust_direction
107 | value = value.split('trustdirection: ')[0]
108 | continue
109 |
110 | current_object[attr] = value
111 |
112 | except Exception as e:
113 | logging.debug(f'Error - {str(e)}')
114 |
115 | return parsed_objects
116 |
117 |
118 | @staticmethod
119 | def _is_start_boundary_line(line):
120 | # BRc4 seems to always have objectClass camelcased, but we'll use lower() just in case
121 | if line.lower().startswith(Brc4LdapSentinelParser.START_BOUNDARY):
122 | return True
123 | return False
124 |
125 |
126 | @staticmethod
127 | def _is_end_boundary_line(line):
128 | if line == Brc4LdapSentinelParser.END_BOUNDARY:
129 | return True
130 | return False
131 |
--------------------------------------------------------------------------------
/bofhound/parsers/generic_parser.py:
--------------------------------------------------------------------------------
1 | import json
2 | import codecs
3 | from .shared_parsers import __all_generic_parsers__
4 |
5 |
6 | class GenericParser:
7 |
8 | def __init__(self):
9 | pass
10 |
11 |
12 | @staticmethod
13 | def parse_file(file, is_outflankc2=False):
14 | with codecs.open(file, 'r', 'utf-8') as f:
15 | if is_outflankc2:
16 | return GenericParser.parse_outflank_file(f.read())
17 | else:
18 | return GenericParser.parse_data(f.read())
19 |
20 |
21 | @staticmethod
22 | def parse_outflank_file(contents):
23 | parsed_objects = []
24 |
25 | for line in contents.splitlines():
26 | event_json = json.loads(line.split('UTC ', 1)[1])
27 |
28 | # we only care about task_resonse events
29 | if event_json['event_type'] != 'task_response':
30 | continue
31 |
32 | # within task_response events, we only care about tasks with specific BOF names
33 | if event_json['task']['name'].lower() not in ['netsession2', 'netloggedon2', 'regsession', 'netlocalgrouplistmembers2']:
34 | continue
35 |
36 | parsed_objects.extend(GenericParser.parse_data(event_json['task']['response']))
37 |
38 | return parsed_objects
39 |
40 |
41 | @staticmethod
42 | def parse_data(contents):
43 | parsed_objects = []
44 | current_parser = None
45 | current_object = {}
46 |
47 | lines = contents.splitlines()
48 |
49 | for line in lines:
50 | # if we have no current parser, check and see if the current line is a start boundary
51 | if current_parser is None:
52 | for parser in __all_generic_parsers__:
53 | if parser.is_start_boundary_line(line):
54 | current_parser = parser
55 | break
56 |
57 | # if we do have a current parser, check and see if the current line is an end boundary
58 | else:
59 | if current_parser is not None:
60 | if current_parser.is_end_boundary_line(line):
61 | # we've reached the end of the current object, so store it and reset the parser
62 | current_object["ObjectType"] = current_parser.OBJECT_TYPE
63 | parsed_objects.append(current_object)
64 | current_parser = None
65 | current_object = {}
66 | continue
67 |
68 | # if we have a current parser and the current line is not an end boundary, parse the line
69 | if current_parser is not None:
70 | current_object = current_parser.parse_line(line, current_object)
71 |
72 | return parsed_objects
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/bofhound/parsers/havoc.py:
--------------------------------------------------------------------------------
1 | import re
2 | import codecs
3 | from bofhound.parsers import LdapSearchBofParser
4 |
5 |
6 | class HavocParser(LdapSearchBofParser):
7 |
8 | @staticmethod
9 | def prep_file(file):
10 | with codecs.open(file, 'r', 'utf-8', errors='ignore') as f:
11 | contents = f.read()
12 |
13 | return re.sub(r'\[\d{2}\/\d{2}\/\d{4} \d{2}:\d{2}:\d{2}\] \[\+\] Received Output \[\d+ bytes\]:\n', '', contents)
--------------------------------------------------------------------------------
/bofhound/parsers/ldap_search_bof.py:
--------------------------------------------------------------------------------
1 | import re
2 | import codecs
3 | import logging
4 | from bofhound.parsers.generic_parser import GenericParser
5 |
6 |
7 | #
8 | # This class will be inherited by other parsers since most if not all are based
9 | # off the same BOF, wrapped by various C2s. These methods can be overridden
10 | # by child classes to handle specific parsing requirements
11 | #
12 | class LdapSearchBofParser():
13 | RESULT_DELIMITER = "-"
14 | RESULT_BOUNDARY_LENGTH = 20
15 | _COMPLETE_BOUNDARY_LINE = -1
16 |
17 |
18 | def __init__(self):
19 | pass
20 |
21 | #
22 | # Legacy, used by test cases for 1 liner
23 | # Removed from __main__.py to avoid duplicating file reads and formatting
24 | #
25 | @staticmethod
26 | def parse_file(file):
27 | return LdapSearchBofParser.parse_data(
28 | LdapSearchBofParser.prep_file(file)
29 | )
30 |
31 |
32 | #
33 | # Replaces parse_file() usage in __main__.py to avoid duplicate file reads
34 | #
35 | @staticmethod
36 | def prep_file(file):
37 | with codecs.open(file, 'r', 'utf-8') as f:
38 | contents = f.read()
39 |
40 | return re.sub(r'\n\n\d{2}\/\d{2} (\d{2}:){2}\d{2} UTC \[output\]\nreceived output:\n', '', contents)
41 |
42 |
43 | #
44 | # Meat of the parsing logic
45 | #
46 | @staticmethod
47 | def parse_data(data):
48 | parsed_objects = []
49 | current_object = None
50 | in_result_region = False
51 | previous_attr = None
52 |
53 | in_result_region = False
54 |
55 | lines = data.splitlines()
56 | for line in lines:
57 | is_boundary_line = LdapSearchBofParser._is_boundary_line(line)
58 |
59 | if (not in_result_region and
60 | not is_boundary_line):
61 | continue
62 |
63 | if (is_boundary_line
64 | and is_boundary_line != LdapSearchBofParser._COMPLETE_BOUNDARY_LINE):
65 | while True:
66 | try:
67 | next_line = next(lines)[1]
68 | remaining_length = LdapSearchBofParser._is_boundary_line(next_line, is_boundary_line)
69 |
70 | if remaining_length:
71 | is_boundary_line = remaining_length
72 | if is_boundary_line == LdapSearchBofParser._COMPLETE_BOUNDARY_LINE:
73 | break
74 | except:
75 | # probably ran past the end of the iterable
76 | break
77 |
78 | if (is_boundary_line):
79 | if not in_result_region:
80 | in_result_region = True
81 | elif current_object is not None:
82 | # self.store_object(current_object)
83 | parsed_objects.append(current_object)
84 | current_object = {}
85 | continue
86 | elif re.match("^(R|r)etr(e|i)(e|i)ved \\d+ results?", line):
87 | #self.store_object(current_object)
88 | parsed_objects.append(current_object)
89 | in_result_region = False
90 | current_object = None
91 | continue
92 |
93 | data = line.split(': ')
94 |
95 | try:
96 | # If we previously encountered a control message, we're probably still in the old property
97 | if len(data) == 1:
98 | if previous_attr is not None:
99 | value = current_object[previous_attr] + line
100 | else:
101 | data = line.split(':')
102 | attr = data[0].strip().lower()
103 | value = ''.join(data[1:]).strip()
104 | previous_attr = attr
105 |
106 | current_object[attr] = value
107 |
108 | except Exception as e:
109 | logging.debug(f'Error - {str(e)}')
110 |
111 | return parsed_objects
112 |
113 |
114 | # Returns one of the following integers:
115 | # 0 - This is not a boundary line
116 | # -1 - This is a complete boundary line
117 | # n - The remaining characters needed to form a complete boundary line
118 | @staticmethod
119 | def _is_boundary_line(line, length=RESULT_BOUNDARY_LENGTH):
120 | line = line.strip()
121 | chars = set(line)
122 |
123 | if len(chars) == 1 and chars.pop() == LdapSearchBofParser.RESULT_DELIMITER:
124 | if len(line) == length:
125 | return -1
126 | elif len(line) < length:
127 | return LdapSearchBofParser.RESULT_BOUNDARY_LENGTH - len(line)
128 |
129 | return 0 # Falsey
130 |
131 |
132 | #
133 | # Get local groups, sessions, etc by feeding data to GenericParser class
134 | #
135 | @staticmethod
136 | def parse_local_objects(data):
137 | return GenericParser.parse_data(data)
--------------------------------------------------------------------------------
/bofhound/parsers/outflankc2.py:
--------------------------------------------------------------------------------
1 | import re
2 | import codecs
3 | import json
4 | import logging
5 | from bofhound.parsers.generic_parser import GenericParser
6 | from bofhound.parsers import LdapSearchBofParser
7 |
8 |
9 | #
10 | # Parses ldapsearch BOF objects from Outflank C2 JSON logfiles
11 | # Assumes that the BOF was registered as a command in OC2 named 'ldapserach'
12 | #
13 |
14 | class OutflankC2JsonParser(LdapSearchBofParser):
15 | BOFNAME = 'ldapsearch'
16 |
17 |
18 | @staticmethod
19 | def prep_file(file):
20 | with codecs.open(file, 'r', 'utf-8') as f:
21 | return f.read()
22 |
23 |
24 | #
25 | # Slightly modified from LdapSearchBofParser to account for
26 | # needing only part of each JSON object, instead of the whole file
27 | #
28 | @staticmethod
29 | def parse_data(contents):
30 | parsed_objects = []
31 | current_object = None
32 | in_result_region = False
33 | previous_attr = None
34 |
35 | in_result_region = False
36 |
37 | lines = contents.splitlines()
38 | for line in lines:
39 | event_json = json.loads(line.split('UTC ', 1)[1])
40 |
41 | # we only care about task_resonse events
42 | if event_json['event_type'] != 'task_response':
43 | continue
44 |
45 | # within task_response events, we only care about tasks with the name 'ldapsearch'
46 | if event_json['task']['name'].lower() != OutflankC2JsonParser.BOFNAME:
47 | continue
48 |
49 | # now we have a block of ldapsearch data we can parse through for objects
50 | response_lines = event_json['task']['response'].splitlines()
51 | for response_line in response_lines:
52 |
53 | is_boundary_line = OutflankC2JsonParser._is_boundary_line(response_line)
54 |
55 | if (not in_result_region and
56 | not is_boundary_line):
57 | continue
58 |
59 | if (is_boundary_line
60 | and is_boundary_line != OutflankC2JsonParser._COMPLETE_BOUNDARY_LINE):
61 | while True:
62 | try:
63 | next_line = next(response_lines)[1]
64 | remaining_length = OutflankC2JsonParser._is_boundary_line(next_line, is_boundary_line)
65 |
66 | if remaining_length:
67 | is_boundary_line = remaining_length
68 | if is_boundary_line == OutflankC2JsonParser._COMPLETE_BOUNDARY_LINE:
69 | break
70 | except:
71 | # probably ran past the end of the iterable
72 | break
73 |
74 | if (is_boundary_line):
75 | if not in_result_region:
76 | in_result_region = True
77 | elif current_object is not None:
78 | # self.store_object(current_object)
79 | parsed_objects.append(current_object)
80 | current_object = {}
81 | continue
82 | elif re.match("^(R|r)etr(e|i)(e|i)ved \\d+ results?", response_line):
83 | #self.store_object(current_object)
84 | parsed_objects.append(current_object)
85 | in_result_region = False
86 | current_object = None
87 | continue
88 |
89 | data = response_line.split(': ')
90 |
91 | try:
92 | # If we previously encountered a control message, we're probably still in the old property
93 | if len(data) == 1:
94 | if previous_attr is not None:
95 | value = current_object[previous_attr] + response_line
96 | else:
97 | data = response_line.split(':')
98 | attr = data[0].strip().lower()
99 | value = ''.join(data[1:]).strip()
100 | previous_attr = attr
101 |
102 | current_object[attr] = value
103 |
104 | except Exception as e:
105 | logging.debug(f'Error - {str(e)}')
106 |
107 | return parsed_objects
108 |
109 |
110 | #
111 | # Get local groups, sessions, etc by feeding data to GenericParser class
112 | #
113 | @staticmethod
114 | def parse_local_objects(file):
115 | return GenericParser.parse_file(file, is_outflankc2=True)
--------------------------------------------------------------------------------
/bofhound/parsers/parsertype.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class ParserType(Enum):
4 | LdapsearchBof = 'ldapsearch'
5 | BRC4 = 'BRC4'
6 | HAVOC = 'Havoc'
7 | OUTFLANKC2 = 'OutflankC2'
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/__init__.py:
--------------------------------------------------------------------------------
1 | from .netloggedon_bof import NetLoggedOnBofParser
2 | from .netsession_bof import NetSessionBofParser
3 | from .netlocalgroup_bof import NetLocalGroupBofParser
4 | from .regsession_bof import RegSessionBofParser
5 |
6 | __all_generic_parsers__ = [
7 | NetLoggedOnBofParser,
8 | NetSessionBofParser,
9 | NetLocalGroupBofParser,
10 | RegSessionBofParser
11 | ]
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/netlocalgroup_bof.py:
--------------------------------------------------------------------------------
1 | from .sharedparser import SharedParser
2 |
3 |
4 | class NetLocalGroupBofParser(SharedParser):
5 | START_BOUNDARY = "----------Local Group Member----------"
6 | END_BOUNDARY = "--------End Local Group Member--------"
7 | OBJECT_TYPE = "LocalGroup"
8 |
9 | def __init__(self):
10 | pass
11 |
12 |
13 | @staticmethod
14 | def is_start_boundary_line(line):
15 | return line.strip() == NetLocalGroupBofParser.START_BOUNDARY
16 |
17 |
18 | @staticmethod
19 | def is_end_boundary_line(line):
20 | return line.strip() == NetLocalGroupBofParser.END_BOUNDARY
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/netloggedon_bof.py:
--------------------------------------------------------------------------------
1 | from .sharedparser import SharedParser
2 |
3 |
4 | class NetLoggedOnBofParser(SharedParser):
5 | START_BOUNDARY = "-----------Logged on User-----------"
6 | END_BOUNDARY = "---------End Logged on User---------"
7 | OBJECT_TYPE = "PrivilegedSession"
8 |
9 | def __init__(self):
10 | pass
11 |
12 |
13 | @staticmethod
14 | def is_start_boundary_line(line):
15 | return line.strip() == NetLoggedOnBofParser.START_BOUNDARY
16 |
17 |
18 | @staticmethod
19 | def is_end_boundary_line(line):
20 | return line.strip() == NetLoggedOnBofParser.END_BOUNDARY
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/netsession_bof.py:
--------------------------------------------------------------------------------
1 | from .sharedparser import SharedParser
2 |
3 |
4 | class NetSessionBofParser(SharedParser):
5 | START_BOUNDARY = "---------------Session--------------"
6 | END_BOUNDARY = "-------------End Session------------"
7 | OBJECT_TYPE = "Session"
8 |
9 | def __init__(self):
10 | pass
11 |
12 |
13 | @staticmethod
14 | def is_start_boundary_line(line):
15 | return line.strip() == NetSessionBofParser.START_BOUNDARY
16 |
17 |
18 | @staticmethod
19 | def is_end_boundary_line(line):
20 | return line.strip() == NetSessionBofParser.END_BOUNDARY
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/regsession_bof.py:
--------------------------------------------------------------------------------
1 | from .sharedparser import SharedParser
2 |
3 |
4 | class RegSessionBofParser(SharedParser):
5 | START_BOUNDARY = "-----------Registry Session---------"
6 | END_BOUNDARY = "---------End Registry Session-------"
7 | OBJECT_TYPE = "RegistrySession"
8 |
9 | def __init__(self):
10 | pass
11 |
12 |
13 | @staticmethod
14 | def is_start_boundary_line(line):
15 | return line.strip() == RegSessionBofParser.START_BOUNDARY
16 |
17 |
18 | @staticmethod
19 | def is_end_boundary_line(line):
20 | return line.strip() == RegSessionBofParser.END_BOUNDARY
--------------------------------------------------------------------------------
/bofhound/parsers/shared_parsers/sharedparser.py:
--------------------------------------------------------------------------------
1 |
2 | class SharedParser():
3 |
4 | def __init__(self):
5 | pass
6 |
7 |
8 | # will be same for all child classes
9 | @staticmethod
10 | def parse_line(line, current_object):
11 | data = line.split(': ')
12 | try:
13 | attr = data[0].strip()
14 | value = data[1].strip()
15 |
16 | # if attr is not on the current object, add it
17 | if attr not in current_object:
18 | current_object[attr] = value
19 |
20 | return current_object
21 | except IndexError:
22 | return current_object
23 |
24 |
25 | # will be implemented in child classes
26 | @staticmethod
27 | def is_start_boundary_line(line):
28 | pass
29 |
30 |
31 | # will be implemented in child classes
32 | @staticmethod
33 | def is_end_boundary_line(line):
34 | pass
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [tool.poetry]
2 | name = "bofhound"
3 | version = "0.4.8"
4 | description = "Parse output from common sources and transform it into BloodHound-ingestible data"
5 | authors = [
6 | "Adam Brown",
7 | "Matt Creel"
8 | ]
9 | readme = "README.md"
10 | homepage = "https://github.com/coffeegist/bofhound"
11 | repository = "https://github.com/coffeegist/bofhound"
12 | include = ["CHANGELOG.md"]
13 |
14 | [tool.poetry.dependencies]
15 | python = "^3.10"
16 | asn1crypto = "^1.5.1"
17 | click = "^8.0"
18 | typer = "^0.9"
19 | bloodhound = "^1.6"
20 | chardet = "^4.0"
21 | cryptography = "^36.0"
22 | dnspython = "^2.2"
23 | Flask = "^2.0"
24 | future = "^0.18.2"
25 | impacket = "^0.10.0"
26 | itsdangerous = "^2.0.1"
27 | Jinja2 = "^3.0.3"
28 | ldap3 = "^2.9.1"
29 | ldapdomaindump = "^0.9.3"
30 | MarkupSafe = "^2.0.1"
31 | pyasn1 = "^0.4.8"
32 | pycparser = "^2.21"
33 | pycryptodomex = "^3.14.0"
34 | pyOpenSSL = "^22.0.0"
35 | six = "^1.16.0"
36 | Werkzeug = "^2.0.2"
37 | rich = "^12.5"
38 | cffi = "^1.17.1"
39 |
40 | [tool.poetry.group.dev.dependencies]
41 | pylint = "^2.13"
42 | pytest = "^7.1.2"
43 |
44 | [build-system]
45 | requires = ["poetry-core>=1.0.0"]
46 | build-backend = "poetry.core.masonry.api"
47 |
48 | [tool.poetry.scripts]
49 | bofhound = "bofhound.__main__:app"
50 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coffeegist/bofhound/fed5d2b63fc38f178423dfb1f77e2f6e9d337634/tests/__init__.py
--------------------------------------------------------------------------------
/tests/ad/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coffeegist/bofhound/fed5d2b63fc38f178423dfb1f77e2f6e9d337634/tests/ad/__init__.py
--------------------------------------------------------------------------------
/tests/ad/models/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coffeegist/bofhound/fed5d2b63fc38f178423dfb1f77e2f6e9d337634/tests/ad/models/__init__.py
--------------------------------------------------------------------------------
/tests/ad/models/test_bloodhound_computer.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.ad.models.bloodhound_computer import BloodHoundComputer
3 | from bofhound.ad.models.bloodhound_object import BloodHoundObject
4 | from bloodhound.enumeration.acls import SecurityDescriptor, ACL, ACCESS_ALLOWED_ACE, ACCESS_MASK, ACE, ACCESS_ALLOWED_OBJECT_ACE, has_extended_right, EXTRIGHTS_GUID_MAPPING, can_write_property, ace_applies
5 |
6 |
7 | @pytest.fixture
8 | def parsed_partial_computer():
9 | pass
10 |
11 |
12 | @pytest.fixture
13 | def parsed_full_computer():
14 | yield {
15 | "objectclass": "top, person, organizationalPerson, user, computer",
16 | "cn": "WIN10",
17 | "distinguishedname": "CN=WIN10,OU=Workstations,DC=windomain,DC=local",
18 | "instancetype": "4",
19 | "whencreated": "20220112013543.0Z",
20 | "whenchanged": "20220401134507.0Z",
21 | "usncreated": "14202",
22 | "usnchanged": "24046",
23 | "ntsecuritydescriptor": "AQAEjJgJAAC0CQAAAAAAABQAAAAEAIQJMQAAAAUASAAgAAAAAwAAABAgIF+ledARkCAAwE/C1M+Gepa/5g3QEaKFAKoAMEniAQUAAAAAAAUVAAAANowB26uPcGmReUlF6AMAAAUASAAgAAAAAwAAAFB5lr/mDdARooUAqgAwSeKGepa/5g3QEaKFAKoAMEniAQUAAAAAAAUVAAAANowB26uPcGmReUlF6AMAAAUASAAgAAAAAwAAAFN5lr/mDdARooUAqgAwSeKGepa/5g3QEaKFAKoAMEniAQUAAAAAAAUVAAAANowB26uPcGmReUlF6AMAAAUASAAgAAAAAwAAANC/Cj5qEtARoGAAqgBsM+2Gepa/5g3QEaKFAKoAMEniAQUAAAAAAAUVAAAANowB26uPcGmReUlF6AMAAAUAOAAIAAAAAQAAAEeV43IYe9ERre8AwE/Y1c0BBQAAAAAABRUAAAA2jAHbq49waZF5SUXoAwAABQA4AAgAAAABAAAAiEem8wZT0RGpxQAA+ANnwQEFAAAAAAAFFQAAADaMAdurj3BpkXlJRegDAAAFADgAIAAAAAEAAAAAQhZMwCDQEadoAKoAbgUpAQUAAAAAAAUVAAAANowB26uPcGmReUlF6AMAAAUAOAAwAAAAAQAAAH96lr/mDdARooUAqgAwSeIBBQAAAAAABRUAAAA2jAHbq49waZF5SUUFAgAABQAsAAMAAAABAAAAqHqWv+YN0BGihQCqADBJ4gECAAAAAAAFIAAAACYCAAAFACwAEAAAAAEAAAAdsalGrmBaQLfo/4pY1FbSAQIAAAAAAAUgAAAAMAIAAAUAKAAAAQAAAQAAAFMacqsvHtARmBkAqgBAUpsBAQAAAAAAAQAAAAAFACgACAAAAAEAAABHleNyGHvREa3vAMBP2NXNAQEAAAAAAAUKAAAABQAoAAgAAAABAAAAiEem8wZT0RGpxQAA+ANnwQEBAAAAAAAFCgAAAAUAKAAwAAAAAQAAAIa4tXdKlNERrr0AAPgDZ8EBAQAAAAAABQoAAAAAACQA1AEDAAEFAAAAAAAFFQAAADaMAdurj3BpkXlJRegDAAAAACQA/wEPAAEFAAAAAAAFFQAAADaMAdurj3BpkXlJRQACAAAAABgA/wEPAAECAAAAAAAFIAAAACQCAAAAABQAAwAAAAEBAAAAAAAFCgAAAAAAFACUAAIAAQEAAAAAAAULAAAAAAAUAP8BDwABAQAAAAAABRIAAAAFEjgAIAAAAAMAAABbspQaIAi6R53LgK7637NwhnqWv+YN0BGihQCqADBJ4gEBAAAAAAAFCgAAAAUSOAAwAAAAAwAAAGL91v7f+9lBsl8a2z53q3eGepa/5g3QEaKFAKoAMEniAQEAAAAAAAUKAAAABRo8ABAAAAADAAAAAEIWTMAg0BGnaACqAG4FKRTMKEg3FLxFmwetbwFeXygBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAAEIWTMAg0BGnaACqAG4FKbp6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAECAgX6V50BGQIADAT8LUzxTMKEg3FLxFmwetbwFeXygBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAECAgX6V50BGQIADAT8LUz7p6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAQMIKvKl50BGQIADAT8LUzxTMKEg3FLxFmwetbwFeXygBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAQMIKvKl50BGQIADAT8LUz7p6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAQi+6WaJ50BGQIADAT8LTzxTMKEg3FLxFmwetbwFeXygBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAAQi+6WaJ50BGQIADAT8LTz7p6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAA+IhwA+EK0hG0IgCgyWj5ORTMKEg3FLxFmwetbwFeXygBAgAAAAAABSAAAAAqAgAABRo8ABAAAAADAAAA+IhwA+EK0hG0IgCgyWj5Obp6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRI4ADAAAAABAAAAD9ZHW5BgskCfNypN6I8wYwEFAAAAAAAFFQAAADaMAdurj3BpkXlJRQ4CAAAFEjgAMAAAAAEAAAAP1kdbkGCyQJ83Kk3ojzBjAQUAAAAAAAUVAAAANowB26uPcGmReUlFDwIAAAUQOAAIAAAAAQAAAKZtAps8DVxGi+5RmdcWXLoBBQAAAAAABRUAAAA2jAHbq49waZF5SUXoAwAABRo4AAgAAAADAAAApm0CmzwNXEaL7lGZ1xZcuoZ6lr/mDdARooUAqgAwSeIBAQAAAAAAAwAAAAAFEjgACAAAAAMAAACmbQKbPA1cRovuUZnXFly6hnqWv+YN0BGihQCqADBJ4gEBAAAAAAAFCgAAAAUSOAAQAAAAAwAAAG2exrfHLNIRhU4AoMmD9giGepa/5g3QEaKFAKoAMEniAQEAAAAAAAUJAAAABRo4ABAAAAADAAAAbZ7Gt8cs0hGFTgCgyYP2CJx6lr/mDdARooUAqgAwSeIBAQAAAAAABQkAAAAFGjgAEAAAAAMAAABtnsa3xyzSEYVOAKDJg/YIunqWv+YN0BGihQCqADBJ4gEBAAAAAAAFCQAAAAUSOAAgAAAAAwAAAJN7G+pIXtVGvGxN9P2nijWGepa/5g3QEaKFAKoAMEniAQEAAAAAAAUKAAAABRosAJQAAgACAAAAFMwoSDcUvEWbB61vAV5fKAECAAAAAAAFIAAAACoCAAAFGiwAlAACAAIAAACcepa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUaLACUAAIAAgAAALp6lr/mDdARooUAqgAwSeIBAgAAAAAABSAAAAAqAgAABRMoADAAAAABAAAA5cN4P5r3vUaguJ0YEW3ceQEBAAAAAAAFCgAAAAUSKAAwAQAAAQAAAN5H5pFv2XBLlVfWP/TzzNgBAQAAAAAABQoAAAAAEiQA/wEPAAEFAAAAAAAFFQAAADaMAdurj3BpkXlJRQcCAAAAEhgABAAAAAECAAAAAAAFIAAAACoCAAAAEhgAvQEPAAECAAAAAAAFIAAAACACAAABBQAAAAAABRUAAAA2jAHbq49waZF5SUXoAwAAAQUAAAAAAAUVAAAANowB26uPcGmReUlFAQIAAA==",
24 | "name": "WIN10",
25 | "objectguid": "f981e173-4db8-48f9-9c2d-3d4987698505",
26 | "useraccountcontrol": "4096",
27 | "badpwdcount": "0",
28 | "codepage": "0",
29 | "countrycode": "0",
30 | "badpasswordtime": "0",
31 | "lastlogoff": "0",
32 | "lastlogon": "132933148216284771",
33 | "localpolicyflags": "0",
34 | "pwdlastset": "132888464114765330",
35 | "primarygroupid": "515",
36 | "objectsid": "S-1-5-21-3674311734-1768984491-1162443153-1104",
37 | "accountexpires": "9223372036854775807",
38 | "logoncount": "222",
39 | "samaccountname": "WIN10$",
40 | "samaccounttype": "805306369",
41 | "operatingsystem": "Windows 10 Enterprise Evaluation",
42 | "operatingsystemversion": "10.0 (18363)",
43 | "dnshostname": "win10.windomain.local",
44 | "serviceprincipalname": "WSMAN/win10, WSMAN/win10.windomain.local, TERMSRV/WIN10, TERMSRV/win10.windomain.local, RestrictedKrbHost/WIN10, HOST/WIN10, RestrictedKrbHost/win10.windomain.local, HOST/win10.windomain.local",
45 | "objectcategory": "CN=Computer,CN=Schema,CN=Configuration,DC=windomain,DC=local",
46 | "iscriticalsystemobject": "FALSE",
47 | "dscorepropagationdata": "20220325174020.0Z, 16010101000001.0Z",
48 | "lastlogontimestamp": "132932943074365707",
49 | "msds-supportedencryptiontypes": "28",
50 | "ms-mcs-admpwd": "testpassword",
51 | "ms-mcs-admpwdexpirationtime": "132953152469914742"
52 | }
53 |
54 |
55 | def test_constructor_hasLaps(parsed_full_computer):
56 | bhc = BloodHoundComputer(parsed_full_computer)
57 |
58 | assert bhc.Properties['haslaps'] == True
59 | assert bhc.Properties['operatingsystem'] == 'Windows 10 Enterprise Evaluation'
--------------------------------------------------------------------------------
/tests/ad/models/test_bloodhound_crossref.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.ad.models.bloodhound_crossref import BloodHoundCrossRef
3 |
4 | @pytest.fixture
5 | def parsed_crossref():
6 | yield {
7 | "cn": "REDANIA",
8 | "dcsorepropagationdata": "16010101000000.0Z",
9 | "distinguishedname": "CN=REDANIA,CN=Partitions,CN=Configuration,DC=redania,DC=local",
10 | "dnsroot": "redania.local",
11 | "instancetype": "4",
12 | "msds-behavior-version": "7",
13 | "ncname": "DC=redania,DC=local",
14 | "netbiosname": "REDANIA",
15 | "ntmixeddomain": "0",
16 | "name": "REDANIA",
17 | "objectcategory": "CN=Cross-Ref,CN=Schema,CN=Configuration,DC=redania,DC=local",
18 | "objectclass": "top, crossRef",
19 | "objectguid": "f66cd454-5cf0-41c2-83c4-743ce81fb33e",
20 | "showinadvancedviewonly": "True",
21 | "systemflags": "3",
22 | "usnchanged": "12565",
23 | "usncreated": "4118",
24 | "whenchanged": "20230214042300.0Z",
25 | "whencreated": "20230214042103.0Z",
26 | }
27 |
28 |
29 | def test_crossref_constructor(parsed_crossref):
30 | cross_ref = BloodHoundCrossRef(parsed_crossref)
31 |
32 | assert cross_ref.netBiosName == "REDANIA"
33 | assert cross_ref.nCName == "DC=REDANIA,DC=LOCAL"
34 | assert cross_ref.distinguishedName == "CN=REDANIA,CN=PARTITIONS,CN=CONFIGURATION,DC=REDANIA,DC=LOCAL"
--------------------------------------------------------------------------------
/tests/ad/models/test_bloodhound_object.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.ad.models.bloodhound_object import BloodHoundObject
3 |
4 |
5 | @pytest.fixture
6 | def parsed_full_user():
7 | yield {
8 | 'objectclass': 'top, person, organizationalPerson, user',
9 | 'cn': 'Administrator',
10 | 'description': 'Built-in account for administering the computer/domain',
11 | 'distinguishedname': 'CN=Administrator,CN=Users,DC=test,DC=lab',
12 | 'instancetype': '4',
13 | 'whencreated': '20210826173042.0Z',
14 | 'whenchanged': '20220403141221.0Z',
15 | 'usncreated': '8196',
16 | 'memberof': 'CN=Group Policy Creator Owners,CN=Users,DC=test,DC=lab, CN=Domain Admins,CN=Users,DC=test,DC=lab, CN=Enterprise Admins,CN=Users,DC=test,DC=lab, CN=Schema Admins,CN=Users,DC=test,DC=lab, CN=Administrators,CN=Builtin,DC=test,DC=lab',
17 | 'usnchanged': '63985',
18 | 'ntsecuritydescriptor': 'AQAEnIgEAACkBAAAAAAAABQAAAAEAHQEGAAAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C088UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C08+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+TkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+Tm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAOAAwAAAAAQAAAH96lr/mDdARooUAqgAwSeIBBQAAAAAABRUAAAB/ivvSK592RVonQNMFAgAABQAsABAAAAABAAAAHbGpRq5gWkC36P+KWNRW0gECAAAAAAAFIAAAADACAAAFACwAMAAAAAEAAAAcmrZtIpTREa69AAD4A2fBAQIAAAAAAAUgAAAAMQIAAAUALAAwAAAAAQAAAGK8BVjJvShEpeKFag9MGF4BAgAAAAAABSAAAAAxAgAABQAsAJQAAgACAAAAFMwoSDcUvEWbB61vAV5fKAECAAAAAAAFIAAAACoCAAAFACwAlAACAAIAAAC6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAKAAAAQAAAQAAAFMacqsvHtARmBkAqgBAUpsBAQAAAAAAAQAAAAAFACgAAAEAAAEAAABTGnKrLx7QEZgZAKoAQFKbAQEAAAAAAAUKAAAABQIoADABAAABAAAA3kfmkW/ZcEuVV9Y/9PPM2AEBAAAAAAAFCgAAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAAGAC/AQ8AAQIAAAAAAAUgAAAAIAIAAAAAFACUAAIAAQEAAAAAAAULAAAAAAAUAP8BDwABAQAAAAAABRIAAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAAAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAA==',
19 | 'name': 'Administrator',
20 | 'objectguid': '7b79190e-285c-40fc-8300-0584b3ee974b',
21 | 'useraccountcontrol': '66048',
22 | 'badpwdcount': '0',
23 | 'codepage': '0',
24 | 'countrycode': '0',
25 | 'badpasswordtime': '132919604027661803',
26 | 'lastlogoff': '0',
27 | 'lastlogon': '132940422420644609',
28 | 'logonhours': '?????????????????????',
29 | 'pwdlastset': '132836191102481334',
30 | 'primarygroupid': '513',
31 | 'objectsid': 'S-1-5-21-3539700351-1165401899-3544196954-500',
32 | 'admincount': '1',
33 | 'accountexpires': '0',
34 | 'logoncount': '208',
35 | 'samaccountname': 'Administrator',
36 | 'samaccounttype': '805306368',
37 | 'objectcategory': 'CN=Person,CN=Schema,CN=Configuration,DC=test,DC=lab',
38 | 'iscriticalsystemobject': 'TRUE',
39 | 'dscorepropagationdata': '20210826202656.0Z, 20210826202656.0Z, 20210826175542.0Z, 16010101181216.0Z',
40 | 'lastlogontimestamp': '132934687411151999',
41 | 'msds-supportedencryptiontypes': '0'
42 | }
43 |
44 |
45 | def test_constructor_firstEmptyObject():
46 | bho = BloodHoundObject()
47 |
48 |
49 | def test_constructor_basicFullObject(parsed_full_user):
50 | bho = BloodHoundObject(parsed_full_user)
51 |
52 | assert bho.ObjectIdentifier == 'S-1-5-21-3539700351-1165401899-3544196954-500'
53 | assert bho.get_distinguished_name() == 'CN=ADMINISTRATOR,CN=USERS,DC=TEST,DC=LAB'
54 | assert bho.Properties['whencreated'] == 1629999042
55 |
56 | def test_merge_entry_fullOverwrite():
57 | bho1 = BloodHoundObject({
58 | 'objectsid': '024929',
59 | 'otherproperty': 1
60 | })
61 |
62 | bho2 = BloodHoundObject({
63 | 'objectsid': '024930',
64 | 'otherproperty': 2
65 | })
66 |
67 | bho1.merge_entry(bho2, base_preference=False)
68 | assert bho1.ObjectIdentifier == '024930'
69 | assert bho1.get_property('otherproperty') == 2
70 |
71 |
72 | def test_merge_entry_preferBase():
73 | bho1 = BloodHoundObject({
74 | 'objectsid': '024929',
75 | 'otherproperty': 1
76 | })
77 |
78 | bho2 = BloodHoundObject({
79 | 'objectsid': '024930',
80 | 'otherproperty': 2
81 | })
82 |
83 | bho1.merge_entry(bho2, base_preference=True)
84 | assert bho1.ObjectIdentifier == '024929'
85 | assert bho1.get_property('otherproperty') == 1
86 |
87 |
88 | def test_merge_entry_nonExistentBaseAttribute():
89 | bho1 = BloodHoundObject()
90 | bho2 = BloodHoundObject({
91 | 'objectsid': '024930',
92 | 'otherproperty': 2
93 | })
94 |
95 | bho1.merge_entry(bho2)
96 | assert bho1.ObjectIdentifier == '024930'
97 |
98 |
99 | def test_merge_entry_nonExistentSourceAttribute():
100 | bho1 = BloodHoundObject({
101 | 'objectsid': '024929',
102 | 'otherproperty': 1
103 | })
104 | bho2 = BloodHoundObject()
105 |
106 | bho1.merge_entry(bho2, base_preference=True)
107 | assert bho1.ObjectIdentifier == '024929'
108 |
109 |
110 | def test_merge_entry_emptyBaseAttributes():
111 | bho1 = BloodHoundObject({
112 | 'objectsid': '',
113 | 'distinguishedname': ''
114 | })
115 | bho2 = BloodHoundObject({
116 | 'objectsid': '024929',
117 | 'distinguishedname': 'DC=value'
118 | })
119 |
120 | bho1.merge_entry(bho2, base_preference=True)
121 | assert bho1.ObjectIdentifier == '024929'
122 | assert bho1.get_property('distinguishedname') == 'DC=VALUE'
123 |
124 |
125 | def test_merge_entry_emptySourceAttributes():
126 | bho1 = BloodHoundObject({
127 | 'objectsid': '',
128 | 'distinguishedname': ''
129 | })
130 | bho2 = BloodHoundObject({
131 | 'objectsid': '024929',
132 | 'distinguishedname': 'DC=value'
133 | })
134 |
135 | bho2.merge_entry(bho1)
136 | assert bho2.ObjectIdentifier == '024929'
137 | assert bho2.Properties['distinguishedname'] == 'DC=VALUE'
138 |
--------------------------------------------------------------------------------
/tests/ad/models/test_bloodhound_user.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.ad.models import BloodHoundObject, BloodHoundUser
3 |
4 | @pytest.fixture
5 | def parsed_full_user():
6 | yield {
7 | 'objectclass': 'top, person, organizationalPerson, user',
8 | 'cn': 'Administrator',
9 | 'description': 'Built-in account for administering the computer/domain',
10 | 'distinguishedname': 'CN=Administrator,CN=Users,DC=test,DC=lab',
11 | 'instancetype': '4',
12 | 'whencreated': '20210826173042.0Z',
13 | 'whenchanged': '20220403141221.0Z',
14 | 'usncreated': '8196',
15 | 'memberof': 'CN=GroupPolicy Creator Owners,CN=Users,DC=test,DC=lab, CN=Domain Admins,CN=Users,DC=test,DC=lab, CN=Enterprise Admins,CN=Users,DC=test,DC=lab, CN=Schema Admins,CN=Users,DC=test,DC=lab, CN=Administrators,CN=Builtin,DC=test,DC=lab',
16 | 'usnchanged': '63985',
17 | 'ntsecuritydescriptor': 'AQAEnIgEAACkBAAAAAAAABQAAAAEAHQEGAAAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C088UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C08+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+TkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+Tm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAOAAwAAAAAQAAAH96lr/mDdARooUAqgAwSeIBBQAAAAAABRUAAAB/ivvSK592RVonQNMFAgAABQAsABAAAAABAAAAHbGpRq5gWkC36P+KWNRW0gECAAAAAAAFIAAAADACAAAFACwAMAAAAAEAAAAcmrZtIpTREa69AAD4A2fBAQIAAAAAAAUgAAAAMQIAAAUALAAwAAAAAQAAAGK8BVjJvShEpeKFag9MGF4BAgAAAAAABSAAAAAxAgAABQAsAJQAAgACAAAAFMwoSDcUvEWbB61vAV5fKAECAAAAAAAFIAAAACoCAAAFACwAlAACAAIAAAC6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAKAAAAQAAAQAAAFMacqsvHtARmBkAqgBAUpsBAQAAAAAAAQAAAAAFACgAAAEAAAEAAABTGnKrLx7QEZgZAKoAQFKbAQEAAAAAAAUKAAAABQIoADABAAABAAAA3kfmkW/ZcEuVV9Y/9PPM2AEBAAAAAAAFCgAAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAAGAC/AQ8AAQIAAAAAAAUgAAAAIAIAAAAAFACUAAIAAQEAAAAAAAULAAAAAAAUAP8BDwABAQAAAAAABRIAAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAAAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAA==',
18 | 'name': 'Administrator',
19 | 'objectguid': '7b79190e-285c-40fc-8300-0584b3ee974b',
20 | 'useraccountcontrol': '66048',
21 | 'badpwdcount': '0',
22 | 'codepage': '0',
23 | 'countrycode': '0',
24 | 'badpasswordtime': '132919604027661803',
25 | 'lastlogoff': '0',
26 | 'lastlogon': '132940422420644609',
27 | 'logonhours': '?????????????????????',
28 | 'pwdlastset': '132836191102481334',
29 | 'primarygroupid': '513',
30 | 'objectsid': 'S-1-5-21-3539700351-1165401899-3544196954-500',
31 | 'admincount': '1',
32 | 'accountexpires': '0',
33 | 'logoncount': '208',
34 | 'samaccountname': 'Administrator',
35 | 'samaccounttype': '805306368',
36 | 'objectcategory': 'CN=Person,CN=Schema,CN=Configuration,DC=test,DC=lab',
37 | 'iscriticalsystemobject': 'TRUE',
38 | 'dscorepropagationdata': '20210826202656.0Z, 20210826202656.0Z, 20210826175542.0Z, 16010101181216.0Z',
39 | 'lastlogontimestamp': '132934687411151999',
40 | 'msds-supportedencryptiontypes': '0'
41 | }
42 |
--------------------------------------------------------------------------------
/tests/ad/test_adds.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.ad import ADDS
3 | from bofhound.ad.models import BloodHoundObject, BloodHoundUser, BloodHoundComputer
4 | from tests.test_data import testdata_ldapsearchbof_beacon_257_objects
5 |
6 |
7 | @pytest.fixture
8 | def raw_user():
9 | yield {
10 | 'objectclass': 'top, person, organizationalPerson, user',
11 | 'cn': 'Administrator',
12 | 'distinguishedname': 'CN=Administrator,CN=Users,DC=test,DC=lab',
13 | 'memberof': 'CN=Group Policy Creator Owners,CN=Users,DC=test,DC=lab, CN=Domain Admins,CN=Users,DC=test,DC=lab, CN=Enterprise Admins,CN=Users,DC=test,DC=lab, CN=Schema Admins,CN=Users,DC=test,DC=lab, CN=Administrators,CN=Builtin,DC=test,DC=lab',
14 | 'ntsecuritydescriptor': 'AQAEnIgEAACkBAAAAAAAABQAAAAEAHQEGAAAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAABCFkzAINARp2gAqgBuBSm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAABAgIF+ledARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M8UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEDCCrypedARkCAAwE/C1M+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C088UzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAEIvulmiedARkCAAwE/C08+6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+TkUzChINxS8RZsHrW8BXl8oAQIAAAAAAAUgAAAAKgIAAAUAPAAQAAAAAwAAAPiIcAPhCtIRtCIAoMlo+Tm6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAOAAwAAAAAQAAAH96lr/mDdARooUAqgAwSeIBBQAAAAAABRUAAAB/ivvSK592RVonQNMFAgAABQAsABAAAAABAAAAHbGpRq5gWkC36P+KWNRW0gECAAAAAAAFIAAAADACAAAFACwAMAAAAAEAAAAcmrZtIpTREa69AAD4A2fBAQIAAAAAAAUgAAAAMQIAAAUALAAwAAAAAQAAAGK8BVjJvShEpeKFag9MGF4BAgAAAAAABSAAAAAxAgAABQAsAJQAAgACAAAAFMwoSDcUvEWbB61vAV5fKAECAAAAAAAFIAAAACoCAAAFACwAlAACAAIAAAC6epa/5g3QEaKFAKoAMEniAQIAAAAAAAUgAAAAKgIAAAUAKAAAAQAAAQAAAFMacqsvHtARmBkAqgBAUpsBAQAAAAAAAQAAAAAFACgAAAEAAAEAAABTGnKrLx7QEZgZAKoAQFKbAQEAAAAAAAUKAAAABQIoADABAAABAAAA3kfmkW/ZcEuVV9Y/9PPM2AEBAAAAAAAFCgAAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAAJAC/AQ4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAAGAC/AQ8AAQIAAAAAAAUgAAAAIAIAAAAAFACUAAIAAQEAAAAAAAULAAAAAAAUAP8BDwABAQAAAAAABRIAAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAAAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAA==',
15 | 'name': 'Administrator',
16 | 'objectguid': '7b79190e-285c-40fc-8300-0584b3ee974b',
17 | 'primarygroupid': '513',
18 | 'objectsid': 'S-1-5-21-3539700351-1165401899-3544196954-500',
19 | 'samaccountname': 'Administrator',
20 | 'samaccounttype': '805306368',
21 | 'objectcategory': 'CN=Person,CN=Schema,CN=Configuration,DC=test,DC=lab'
22 | }
23 |
24 |
25 | @pytest.fixture
26 | def raw_trust():
27 | yield {
28 | 'cn': 'child.windomain.local',
29 | 'distinguishedname': 'CN=child.windomain.local,CN=System,DC=windomain,DC=local',
30 | 'flatname': 'CHILD',
31 | 'instancetype': '4',
32 | 'name': 'child.windomain.local',
33 | 'objectcategory': 'CN=Trusted-Domain,CN=Schema,CN=Configuration,DC=windomain,DC=local',
34 | 'objectclass': 'top, leaf, trustedDomain',
35 | 'objectguid': 'ccb3617a-af7a-4671-8c1d-aadbab087df1',
36 | 'trustattributes': '32',
37 | 'trustdirection': '3',
38 | 'trustpartner': 'child.windomain.local',
39 | 'trustposixfffset': '-2147483648',
40 | 'trusttype': '2',
41 | 'securityidentifier': 'S-1-5-21-3539700351-1165401899-3544196955',
42 | }
43 |
44 |
45 | @pytest.fixture
46 | def raw_domain():
47 | yield {
48 | 'creationtime': '132979682115640324',
49 | 'dc': 'windomain',
50 | 'distinguishedname': 'DC=windomain,DC=local',
51 | 'forcelogoff': '-9223372036854775808',
52 | 'gplink': '[LDAP://cn={F08300B1-8BFF-4866-8524-14C03B50D991},cn=policies,cn=system,DC=windomain,DC=local;0][LDAP://cn={79081BF9-A672-4475-A982-D622CC600A49},cn=policies,cn=system,DC=windomain,DC=local;0][LDAP://CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=windomain,DC=local;0]',
53 | 'instancetype': '5',
54 | 'lockoutobservationwindow': '-18000000000',
55 | 'lockoutduration': '-18000000000',
56 | 'lockoutthreshold': '0',
57 | 'masteredBy': 'CN=NTDS Settings,CN=DC1,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN=Configuration,DC=windomain,DC=local',
58 | 'name': 'windomain',
59 | 'nextrid': '1000',
60 | 'objectcategory': 'CN=Domain-DNS,CN=Schema,CN=Configuration,DC=windomain,DC=local',
61 | 'objectclass': 'top, domain, domainDNS',
62 | 'objectguid': '2825688f-74fd-460c-8b32-6b95b149f6ae',
63 | 'objectsid': 'S-1-5-21-3539700351-1165401899-3544196954',
64 | 'otherwellknownobjects': 'B:32:683A24E2E8164BD3AF86AC3C2CF3F981:CN=Keys,DC=windomain,DC=local, B:32:1EB93889E40C45DF9F0C64D23BBB6237:CN=Managed Service Accounts,DC=windomain,DC=local',
65 | 'pwdhistorylength': '24',
66 | 'pwdproperties': '1',
67 | 'ridmanagerreference': 'CN=RID Manager$,CN=System,DC=windomain,DC=local',
68 | 'serverstate': '1',
69 | 'subrefs': 'DC=ForestDnsZones,DC=windomain,DC=local, DC=DomainDnsZones,DC=windomain,DC=local, CN=Configuration,DC=windomain,DC=local',
70 | 'systemflags': '-1946157056',
71 | 'wellknownobjects': 'B:32:6227F0AF1FC2410D8E3BB10615BB5B0F:CN=NTDS Quotas,DC=windomain,DC=local, B:32:F4BE92A4C777485E878E9421D53087DB:CN=Microsoft,CN=Program Data,DC=windomain,DC=local, B:32:09460C08AE1E4A4EA0F64AEE7DAA1E5A:CN=Program Data,DC=windomain,DC=local, B:32:22B70C67D56E4EFB91E9300FCA3DC1AA:CN=ForeignSecurityPrincipals,DC=windomain,DC=local, B:32:18E2EA80684F11D2B9AA00C04F79F805:CN=Deleted Objects,DC=windomain,DC=local, B:32:2FBAC1870ADE11D297C400C04FD8D5CD:CN=Infrastructure,DC=windomain,DC=local, B:32:AB8153B7768811D1ADED00C04FD8D5CD:CN=LostAndFound,DC=windomain,DC=local, B:32:AB1D30F3768811D1ADED00C04FD8D5CD:CN=System,DC=windomain,DC=local, B:32:A361B2FFFFD211D1AA4B00C04FD7D83A:OU=Domain Controllers,DC=windomain,DC=local, B:32:AA312825768811D1ADED00C04FD8D5CD:CN=Computers,DC=windomain,DC=local, B:32:A9D1CA15768811D1ADED00C04FD8D5CD:CN=Users,DC=windomain,DC=local'
72 | }
73 |
74 |
75 | @pytest.fixture
76 | def raw_crossref():
77 | yield {
78 | "cn": "REDANIA",
79 | "dcsorepropagationdata": "16010101000000.0Z",
80 | "distinguishedname": "CN=REDANIA,CN=Partitions,CN=Configuration,DC=redania,DC=local",
81 | "dnsroot": "redania.local",
82 | "instancetype": "4",
83 | "msds-behavior-version": "7",
84 | "ncname": "DC=redania,DC=local",
85 | "netbiosname": "REDANIA",
86 | "ntmixeddomain": "0",
87 | "name": "REDANIA",
88 | "objectcategory": "CN=Cross-Ref,CN=Schema,CN=Configuration,DC=redania,DC=local",
89 | "objectclass": "top, crossRef",
90 | "objectguid": "f66cd454-5cf0-41c2-83c4-743ce81fb33e",
91 | "showinadvancedviewonly": "True",
92 | "systemflags": "3",
93 | "usnchanged": "12565",
94 | "usncreated": "4118",
95 | "whenchanged": "20230214042300.0Z",
96 | "whencreated": "20230214042103.0Z",
97 | }
98 |
99 |
100 | def test_import_objects_singleSchema():
101 | adds = ADDS()
102 | adds.import_objects([{ADDS.AT_SCHEMAIDGUID: 'ABWwRRnE0RG7yQCAx2ZwwA==', ADDS.AT_NAME: 'ANR'}])
103 |
104 | assert len(adds.schemas) == 1
105 |
106 |
107 | def test_import_objects_singleSchema_ldif():
108 | adds = ADDS()
109 | adds.import_objects([{ADDS.AT_SCHEMAIDGUID: '45b01500-c419-11d1-bbc9-0080c76670c0', ADDS.AT_NAME: 'ANR'}])
110 |
111 | assert len(adds.schemas) == 1
112 |
113 |
114 | def test_import_objects_noAccountType(raw_user):
115 | adds = ADDS()
116 | raw_user.pop(ADDS.AT_SAMACCOUNTTYPE)
117 |
118 | adds.import_objects([raw_user])
119 |
120 | assert (len(adds.users) == len(adds.computers) == len(adds.groups) \
121 | == len(adds.trustaccounts) == len(adds.domains) == 0) \
122 | and len(adds.unknown_objects) == 1
123 |
124 |
125 | def test_import_objects_expectedValuesFromStandardDataSet(testdata_ldapsearchbof_beacon_257_objects):
126 | adds = ADDS()
127 | adds.import_objects(testdata_ldapsearchbof_beacon_257_objects)
128 |
129 | assert len(adds.SID_MAP) == 92
130 | assert len(adds.DN_MAP) == 92
131 | assert len(adds.DOMAIN_MAP) == 1
132 | assert len(adds.users) == 5
133 | assert len(adds.computers) == 4
134 | assert len(adds.groups) == 53
135 | assert len(adds.domains) == 1
136 | assert len(adds.schemas) == 0
137 | assert len(adds.trustaccounts) == 0
138 | assert len(adds.ous) == 1
139 | assert len(adds.gpos) == 4
140 | assert len(adds.containers) == 24
141 | assert len(adds.unknown_objects) == 69
142 |
143 |
144 | def test_import_objects_MinimalObject(raw_user):
145 | expected_sid = 'S-1-5-21-3539700351-1165401899-3544196954-500'
146 | expected_dn = 'CN=ADMINISTRATOR,CN=USERS,DC=TEST,DC=LAB'
147 |
148 | adds = ADDS()
149 | adds.import_objects([raw_user])
150 |
151 | sid_map_object = adds.SID_MAP[expected_sid]
152 | dn_map_object = adds.DN_MAP[expected_dn]
153 |
154 | assert len(adds.SID_MAP) == 1
155 | assert sid_map_object.Properties[ADDS.AT_DISTINGUISHEDNAME] == expected_dn
156 | assert dn_map_object.ObjectIdentifier == expected_sid
157 |
158 |
159 | def test_import_objects_DuplicateObject(raw_user):
160 | expected_sid = 'S-1-5-21-3539700351-1165401899-3544196954-500'
161 | expected_dn = 'CN=ADMINISTRATOR,CN=USERS,DC=TEST,DC=LAB'
162 |
163 | adds = ADDS()
164 | adds.import_objects([raw_user, raw_user])
165 |
166 | sid_map_object = adds.SID_MAP[expected_sid]
167 | dn_map_object = adds.DN_MAP[expected_dn]
168 |
169 | assert len(adds.SID_MAP) == 1
170 | assert sid_map_object.Properties[ADDS.AT_DISTINGUISHEDNAME] == expected_dn
171 | assert dn_map_object.ObjectIdentifier == expected_sid
172 |
173 |
174 | def test_import_unique_trust(raw_trust, raw_domain):
175 | expected_domain_count = 1
176 | expected_trust_count = 1
177 |
178 | adds = ADDS()
179 |
180 | adds = ADDS()
181 | adds.import_objects([raw_domain, raw_trust])
182 | adds.process()
183 |
184 | assert len(adds.domains) == expected_domain_count
185 | assert len(adds.domains[0].Trusts) == expected_trust_count
186 |
187 |
188 | def test_import_duplicate_trust(raw_trust, raw_domain):
189 | expected_domain_count = 1
190 | expected_trust_count = 1
191 |
192 | adds = ADDS()
193 |
194 | adds = ADDS()
195 | adds.import_objects([raw_domain, raw_trust, raw_trust])
196 | adds.process()
197 |
198 | assert len(adds.domains) == expected_domain_count
199 | assert len(adds.domains[0].Trusts) == expected_trust_count
200 |
201 |
202 | def test_import_unique_crossref(raw_crossref):
203 | expected_crossref_count = 1
204 |
205 | adds = ADDS()
206 |
207 | adds = ADDS()
208 | adds.import_objects([raw_crossref])
209 |
210 | assert len(adds.CROSSREF_MAP) == expected_crossref_count
211 |
212 |
213 | def test_import_duplicate_crossref(raw_crossref):
214 | expected_crossref_count = 1
215 |
216 | adds = ADDS()
217 |
218 | adds = ADDS()
219 | adds.import_objects([raw_crossref, raw_crossref])
220 |
221 | assert len(adds.CROSSREF_MAP) == expected_crossref_count
--------------------------------------------------------------------------------
/tests/local/test_local_broker.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.local import LocalBroker
3 | from tests.test_data import *
4 |
5 | KNOWN_DOMAIN_SIDS = [
6 | "S-1-5-21-1308756548-3893869957-2915408613"
7 | ]
8 |
9 |
10 | def test_import_netloggedon_objects(netloggedon_redania_objects):
11 | local_broker = LocalBroker()
12 | local_broker.import_objects(netloggedon_redania_objects, KNOWN_DOMAIN_SIDS)
13 |
14 | assert len(local_broker.privileged_sessions) == 1
15 |
16 |
17 | def test_import_netsession_netapi_objects(netsession_redania_netapi_objects):
18 | local_broker = LocalBroker()
19 | local_broker.import_objects(netsession_redania_netapi_objects, KNOWN_DOMAIN_SIDS)
20 |
21 | assert len(local_broker.sessions) == 1
22 |
23 |
24 | def test_import_netsession_netapi_anonymous():
25 | anonymous = {"ObjectType": "Session", "User": "anonymous logon", "ComputerName": "TRETOGOR", "ComputerDomain": "REDANIA"}
26 | local_broker = LocalBroker()
27 | local_broker.import_objects([anonymous], KNOWN_DOMAIN_SIDS)
28 |
29 | assert len(local_broker.sessions) == 0
30 |
31 |
32 | def test_import_netsession_dns_objects(netsession_redania_dns_objects):
33 | local_broker = LocalBroker()
34 | local_broker.import_objects(netsession_redania_dns_objects, KNOWN_DOMAIN_SIDS)
35 |
36 | assert len(local_broker.sessions) == 1
37 |
38 |
39 | def test_import_netsession_dns_anonymous():
40 | anonymous = {"ObjectType": "Session", "User": "anonymous logon", "PTR": "tretogor.redania.local"}
41 | local_broker = LocalBroker()
42 | local_broker.import_objects([anonymous], KNOWN_DOMAIN_SIDS)
43 |
44 | assert len(local_broker.sessions) == 0
45 |
46 |
47 | def test_import_netlocalgroup_objects(netlocalgroup_redania_objects):
48 | local_broker = LocalBroker()
49 | local_broker.import_objects(netlocalgroup_redania_objects, KNOWN_DOMAIN_SIDS)
50 |
51 | assert len(local_broker.local_group_memberships) == 3
52 |
53 |
54 | def test_import_netlocalgroup_invalid_group():
55 | bad_group = {"ObjectType": "LocalGroupMembership", "Member": "Administrator", "Host": "oxenfurt.redania,local", "Group": "BadGroup"}
56 | local_broker = LocalBroker()
57 | local_broker.import_objects([bad_group], KNOWN_DOMAIN_SIDS)
58 |
59 | assert len(local_broker.local_group_memberships) == 0
60 |
61 |
62 | def test_import_regsession_objects(regsession_redania_objects):
63 | local_broker = LocalBroker()
64 | local_broker.import_objects(regsession_redania_objects, KNOWN_DOMAIN_SIDS)
65 |
66 | assert len(local_broker.registry_sessions) == 3
67 |
68 |
69 | def test_objects_with_ip_as_host():
70 | priv_session = {
71 | "ObjectType": "PrivilegedSession",
72 | "Username": "Administrator",
73 | "Domain": "REDANIA",
74 | "Host": "192.168.0.235"
75 | }
76 | registry_session = {
77 | "ObjectType": "RegistrySession",
78 | "UserSid": "S-1-5-21-1308756548-3893869957-2915408613-500",
79 | "Host": "192.168.0.215"
80 | }
81 | local_group_membership = {
82 | "ObjectType": "LocalGroupMembership",
83 | "Host": "192.168.0.215",
84 | "Group": "Administrators",
85 | "Member": "REDANIA\\Domain Admins",
86 | "MemberSid": "S-1-5-21-1308756548-3893869957-2915408613-512",
87 | "MemberSidType": "Group"
88 | }
89 |
90 | # Sessions resolved through NetSessionEnum will not have an IP host
91 |
92 | local_broker = LocalBroker()
93 | local_broker.import_objects(
94 | [
95 | priv_session,
96 | registry_session,
97 | local_group_membership
98 | ],
99 | KNOWN_DOMAIN_SIDS
100 | )
101 |
102 | assert len(local_broker.privileged_sessions) == 0
103 | assert len(local_broker.registry_sessions) == 0
104 | assert len(local_broker.local_group_memberships) == 0
--------------------------------------------------------------------------------
/tests/parsers/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coffeegist/bofhound/fed5d2b63fc38f178423dfb1f77e2f6e9d337634/tests/parsers/__init__.py
--------------------------------------------------------------------------------
/tests/parsers/test_brc4_ldap_sentinel.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from bofhound.ad.models.bloodhound_computer import BloodHoundComputer
4 | from bofhound.parsers.brc4_ldap_sentinel import Brc4LdapSentinelParser
5 | from bofhound.ad.adds import ADDS
6 | from tests.test_data import *
7 |
8 |
9 | def test_parse_file_ldapsearchpyNormalFile(brc4ldapsentinel_standard_file_1030):
10 | parsed_objects = Brc4LdapSentinelParser.parse_file(brc4ldapsentinel_standard_file_1030)
11 | assert len(parsed_objects) == 1030
12 |
13 |
14 | def test_parse_data_computer():
15 | data = """[+] AccountDisabled : FALSE
16 |
17 | +-------------------------------------------------------------------+
18 | [+] objectClass : top; person; organizationalPerson; user; computer
19 | [+] cn : WS1
20 | [+] distinguishedName : CN=WS1,CN=Computers,DC=castle,DC=lab
21 | [+] instanceType : 4
22 | [+] whenCreated : 3/1/2023 5:33:52 PM
23 | [+] whenChanged : 3/1/2023 5:34:27 PM
24 | [+] uSNCreated : high: 0 low: 12880
25 | [+] uSNChanged : high: 0 low: 12894
26 | [+] name : WS1
27 | [+] objectGUID : {7FE62E98-AA0F-4DA0-98DC-80B22ED80B24}
28 | [+] userAccountControl : 4096
29 | [+] badPwdCount : 0
30 | [+] codePage : 0
31 | [+] countryCode : 0
32 | [+] badPasswordTime : Value not set
33 | [+] lastLogoff : Value not set
34 | [+] lastLogon : 3/7/2023 10:14:50 AM
35 | [+] localPolicyFlags : 0
36 | [+] pwdLastSet : 3/1/2023 9:33:52 AM
37 | [+] primaryGroupID : 515
38 | [+] objectSid : S-1-5-21-4033075623-2380760593-384075220-1104
39 | [+] accountExpires : Never expires
40 | [+] logonCount : 23
41 | [+] sAMAccountName : WS1$
42 | [+] sAMAccountType : 805306369
43 | [+] operatingSystem : Windows 10 Pro N
44 | [+] operatingSystemVersion : 10.0 (19045)
45 | [+] dNSHostName : ws1.castle.lab
46 | [+] servicePrincipalName : RestrictedKrbHost/WS1; HOST/WS1; RestrictedKrbHost/ws1.castle.lab; HOST/ws1.castle.lab
47 | [+] objectCategory : CN=Computer,CN=Schema,CN=Configuration,DC=castle,DC=lab
48 | [+] isCriticalSystemObject : FALSE
49 | [+] dSCorePropagationData : 1/1/1601
50 | [+] lastLogonTimestamp : 3/1/2023 9:34:27 AM
51 | [+] msDS-SupportedEncryptionTypes : 28
52 | [+] ADsPath : LDAP://CN=WS1,CN=Computers,DC=castle,DC=lab
53 | [+] PasswordSettings : Never expires
54 | [+] AccountDisabled : FALSE
55 |
56 | +-------------------------------------------------------------------+
57 | """
58 | parsed_objects = Brc4LdapSentinelParser.parse_data(data)
59 |
60 | assert len(parsed_objects) == 1
61 | assert 'operatingsystem' in parsed_objects[0].keys()
62 | assert 'operatingsystem' in BloodHoundComputer(parsed_objects[0]).Properties.keys()
63 |
64 |
65 | def test_parse_lower_data_computer():
66 | data = """[+] accountdisabled : false
67 |
68 | +-------------------------------------------------------------------+
69 | [+] objectclass : top; person; organizationalperson; user; computer
70 | [+] cn : ws1
71 | [+] distinguishedname : cn=ws1,cn=computers,dc=castle,dc=lab
72 | [+] instancetype : 4
73 | [+] whencreated : 3/1/2023 5:33:52 pm
74 | [+] whenchanged : 3/1/2023 5:34:27 pm
75 | [+] usncreated : high: 0 low: 12880
76 | [+] usnchanged : high: 0 low: 12894
77 | [+] name : ws1
78 | [+] objectguid : {7fe62e98-aa0f-4da0-98dc-80b22ed80b24}
79 | [+] useraccountcontrol : 4096
80 | [+] badpwdcount : 0
81 | [+] codepage : 0
82 | [+] countrycode : 0
83 | [+] badpasswordtime : value not set
84 | [+] lastlogoff : value not set
85 | [+] lastlogon : 3/7/2023 10:14:50 am
86 | [+] localpolicyflags : 0
87 | [+] pwdlastset : 3/1/2023 9:33:52 am
88 | [+] primarygroupid : 515
89 | [+] objectsid : s-1-5-21-4033075623-2380760593-384075220-1104
90 | [+] accountexpires : never expires
91 | [+] logoncount : 23
92 | [+] samaccountname : ws1$
93 | [+] samaccounttype : 805306369
94 | [+] operatingsystem : windows 10 pro n
95 | [+] operatingsystemversion : 10.0 (19045)
96 | [+] dnshostname : ws1.castle.lab
97 | [+] serviceprincipalname : restrictedkrbhost/ws1; host/ws1; restrictedkrbhost/ws1.castle.lab; host/ws1.castle.lab
98 | [+] objectcategory : cn=computer,cn=schema,cn=configuration,dc=castle,dc=lab
99 | [+] iscriticalsystemobject : false
100 | [+] dscorepropagationdata : 1/1/1601
101 | [+] lastlogontimestamp : 3/1/2023 9:34:27 am
102 | [+] msds-supportedencryptiontypes : 28
103 | [+] adspath : ldap://cn=ws1,cn=computers,dc=castle,dc=lab
104 | [+] passwordsettings : never expires
105 | [+] accountdisabled : false
106 |
107 | +-------------------------------------------------------------------+
108 | """
109 | parsed_objects = Brc4LdapSentinelParser.parse_data(data)
110 | ad = ADDS()
111 | ad.import_objects(parsed_objects)
112 |
113 | assert len(parsed_objects) == 1
114 | assert 'operatingsystem' in parsed_objects[0].keys()
115 | assert 'operatingsystem' in BloodHoundComputer(parsed_objects[0]).Properties.keys()
116 | assert len(ad.computers) == 1
117 |
118 |
119 | def test_parse_data_computer_data_missing_dn():
120 | data = """[+] AccountDisabled : FALSE
121 |
122 | +-------------------------------------------------------------------+
123 | [+] objectClass : top; person; organizationalPerson; user; computer
124 | [+] cn : WS1
125 | [+] instanceType : 4
126 | [+] whenCreated : 3/1/2023 5:33:52 PM
127 | [+] whenChanged : 3/1/2023 5:34:27 PM
128 | [+] uSNCreated : high: 0 low: 12880
129 | [+] uSNChanged : high: 0 low: 12894
130 | [+] name : WS1
131 | [+] objectGUID : {7FE62E98-AA0F-4DA0-98DC-80B22ED80B24}
132 | [+] userAccountControl : 4096
133 | [+] badPwdCount : 0
134 | [+] codePage : 0
135 | [+] countryCode : 0
136 | [+] badPasswordTime : Value not set
137 | [+] lastLogoff : Value not set
138 | [+] lastLogon : 3/7/2023 10:14:50 AM
139 | [+] localPolicyFlags : 0
140 | [+] pwdLastSet : 3/1/2023 9:33:52 AM
141 | [+] primaryGroupID : 515
142 | [+] objectSid : S-1-5-21-4033075623-2380760593-384075220-1104
143 | [+] accountExpires : Never expires
144 | [+] logonCount : 23
145 | [+] sAMAccountName : WS1$
146 | [+] sAMAccountType : 805306369
147 | [+] operatingSystem : Windows 10 Pro N
148 | [+] operatingSystemVersion : 10.0 (19045)
149 | [+] dNSHostName : ws1.castle.lab
150 | [+] servicePrincipalName : RestrictedKrbHost/WS1; HOST/WS1; RestrictedKrbHost/ws1.castle.lab; HOST/ws1.castle.lab
151 | [+] objectCategory : CN=Computer,CN=Schema,CN=Configuration,DC=castle,DC=lab
152 | [+] isCriticalSystemObject : FALSE
153 | [+] dSCorePropagationData : 1/1/1601
154 | [+] lastLogonTimestamp : 3/1/2023 9:34:27 AM
155 | [+] msDS-SupportedEncryptionTypes : 28
156 | [+] ADsPath : LDAP://CN=WS1,CN=Computers,DC=castle,DC=lab
157 | [+] PasswordSettings : Never expires
158 | [+] AccountDisabled : FALSE
159 |
160 | +-------------------------------------------------------------------+
161 | """
162 | parsed_objects = Brc4LdapSentinelParser.parse_data(data)
163 | ad = ADDS()
164 | ad.import_objects(parsed_objects)
165 |
166 | assert len(parsed_objects) == 1
167 | # this test is failing - should distinguishedname be required?
168 | assert len(ad.computers) == 0
169 |
170 |
171 | # This test case currently is not possible with BRc4,
172 | # since all attributes are returned by default
173 |
174 | '''
175 | def test_parse_mininal_data_computer():
176 | data = """[+] accountdisabled : false
177 |
178 | +-------------------------------------------------------------------+
179 | [+] distinguishedname : cn=ws1,cn=computers,dc=castle,dc=lab
180 | [+] objectsid : s-1-5-21-4033075623-2380760593-384075220-1104
181 | [+] samaccounttype : 805306369
182 |
183 | +-------------------------------------------------------------------+
184 | """
185 | parsed_objects = Brc4LdapSentinelParser.parse_data(data)
186 | ad = ADDS()
187 | ad.import_objects(parsed_objects)
188 |
189 | assert len(parsed_objects) == 1
190 | assert len(ad.computers) == 1
191 | '''
192 |
--------------------------------------------------------------------------------
/tests/parsers/test_generic_parser.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from bofhound.parsers.generic_parser import GenericParser
3 | from bofhound.parsers.shared_parsers import NetLoggedOnBofParser, NetSessionBofParser, NetLocalGroupBofParser, RegSessionBofParser
4 | from tests.test_data import *
5 |
6 |
7 | def test_parse_file_netloggedon_redania(netloggedon_redania_file):
8 | parsed_objects = GenericParser.parse_file(netloggedon_redania_file)
9 | assert len(parsed_objects) == 12
10 |
11 |
12 | def test_parse_file_netsession_redania_netapi(netsession_redania_netapi_file):
13 | parsed_objects = GenericParser.parse_file(netsession_redania_netapi_file)
14 | assert len(parsed_objects) == 2
15 |
16 |
17 | def test_parse_file_netsession_redania_dns(netsession_redania_dns_file):
18 | parsed_objects = GenericParser.parse_file(netsession_redania_dns_file)
19 | assert len(parsed_objects) == 2
20 |
21 |
22 | def test_parse_file_netlocalgroup_redania(netlocalgroup_redania_file):
23 | parsed_objects = GenericParser.parse_file(netlocalgroup_redania_file)
24 | assert len(parsed_objects) == 5
25 |
26 |
27 | def test_parse_file_regsession_redania(regsession_redania_file):
28 | parsed_objects = GenericParser.parse_file(regsession_redania_file)
29 | assert len(parsed_objects) == 4
30 |
31 |
32 | def test_parsed_object_types(netloggedon_redania_file, netsession_redania_netapi_file, netsession_redania_dns_file, netlocalgroup_redania_file, regsession_redania_file):
33 | parsed_privsessions_objects = GenericParser.parse_file(netloggedon_redania_file)
34 | parsed_session_netapi_objects = GenericParser.parse_file(netsession_redania_netapi_file)
35 | parsed_session_dns_objects = GenericParser.parse_file(netsession_redania_dns_file)
36 | parsed_localgroup_objects = GenericParser.parse_file(netlocalgroup_redania_file)
37 | parsed_regsession_objects = GenericParser.parse_file(regsession_redania_file)
38 |
39 | assert parsed_privsessions_objects[0]["ObjectType"] == NetLoggedOnBofParser.OBJECT_TYPE
40 | assert parsed_session_netapi_objects[0]["ObjectType"] == NetSessionBofParser.OBJECT_TYPE
41 | assert parsed_session_dns_objects[1]["ObjectType"] == NetSessionBofParser.OBJECT_TYPE
42 | assert parsed_localgroup_objects[1]["ObjectType"] == NetLocalGroupBofParser.OBJECT_TYPE
43 | assert parsed_regsession_objects[0]["ObjectType"] == RegSessionBofParser.OBJECT_TYPE
--------------------------------------------------------------------------------
/tests/test_data/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pytest
3 | from bofhound.parsers import LdapSearchBofParser
4 | from bofhound.parsers.generic_parser import GenericParser
5 | from bofhound.ad import ADDS
6 | from bofhound.local import LocalBroker
7 |
8 | TEST_DATA_DIR = os.path.abspath(
9 | os.path.join(
10 | os.path.dirname(os.path.abspath(__file__)),
11 | "..",
12 | "test_data"
13 | )
14 | )
15 |
16 | # LdapSearchPY Fixtures
17 | @pytest.fixture
18 | def ldapsearchpy_standard_file_516():
19 | yield os.path.join(TEST_DATA_DIR, "ldapsearchpy_logs/ldapsearch_516-objects.log")
20 |
21 |
22 | # LdapSearchBOF Fixtures
23 | @pytest.fixture
24 | def ldapsearchbof_standard_file_257():
25 | yield os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_257-objects.log")
26 |
27 |
28 | @pytest.fixture
29 | def ldapsearchbof_standard_file_202():
30 | yield os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_202.log")
31 |
32 |
33 | @pytest.fixture
34 | def testdata_ldapsearchbof_beacon_257_objects():
35 | log_file = os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_257-objects.log")
36 | yield LdapSearchBofParser.parse_file(log_file)
37 |
38 |
39 | @pytest.fixture
40 | def testdata_ldapsearchbof_beacon_202_objects():
41 | log_file = os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_202.log")
42 | yield LdapSearchBofParser.parse_file(log_file)
43 |
44 |
45 | @pytest.fixture
46 | def testdata_pyldapsearch_redania_objects():
47 | log_file = os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/pyldapsearch_redania_objects.log")
48 | yield LdapSearchBofParser.parse_file(log_file)
49 |
50 |
51 | @pytest.fixture
52 | def testdata_marvel_ldap_objects():
53 | log_file = os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_marvel_ldap_sessions_localgroup.log")
54 | yield LdapSearchBofParser.parse_file(log_file)
55 |
56 |
57 | @pytest.fixture
58 | def testdata_marvel_local_objects():
59 | log_file = os.path.join(TEST_DATA_DIR, "ldapsearchbof_logs/beacon_marvel_ldap_sessions_localgroup.log")
60 | yield GenericParser.parse_file(log_file)
61 |
62 |
63 | # BRc4 LDAP Sentinel Fixtures
64 | @pytest.fixture
65 | def brc4ldapsentinel_standard_file_1030():
66 | yield os.path.join(TEST_DATA_DIR, "brc4_ldap_sentinel_logs/badger_no_acl_1030_objects.log")
67 |
68 |
69 | #### Generic Parser Fixtures
70 |
71 | # NetLoggedOn BOF Fixtures
72 | @pytest.fixture
73 | def netloggedon_redania_file():
74 | yield os.path.join(TEST_DATA_DIR, "netloggedonbof_logs/netloggedonbof_redania.log")
75 |
76 |
77 | @pytest.fixture
78 | def netloggedon_redania_objects():
79 | log_file = os.path.join(TEST_DATA_DIR, "netloggedonbof_logs/netloggedonbof_redania.log")
80 | yield GenericParser.parse_file(log_file)
81 |
82 | # NetSession BOF Fixtures
83 | @pytest.fixture
84 | def netsession_redania_netapi_file():
85 | yield os.path.join(TEST_DATA_DIR, "netsessionbof_logs/netsessionbof_redania_netapi.log")
86 |
87 |
88 | @pytest.fixture
89 | def netsession_redania_netapi_objects():
90 | log_file = os.path.join(TEST_DATA_DIR, "netsessionbof_logs/netsessionbof_redania_netapi.log")
91 | yield GenericParser.parse_file(log_file)
92 |
93 |
94 | @pytest.fixture
95 | def netsession_redania_dns_file():
96 | yield os.path.join(TEST_DATA_DIR, "netsessionbof_logs/netsessionbof_redania_dns.log")
97 |
98 |
99 | @pytest.fixture
100 | def netsession_redania_dns_objects():
101 | log_file = os.path.join(TEST_DATA_DIR, "netsessionbof_logs/netsessionbof_redania_dns.log")
102 | yield GenericParser.parse_file(log_file)
103 |
104 | # NetLocalGroup BOF Fixtures
105 | @pytest.fixture
106 | def netlocalgroup_redania_file():
107 | yield os.path.join(TEST_DATA_DIR, "netlocalgroupbof_logs/netlocalgroupbof_redania.log")
108 |
109 |
110 | @pytest.fixture
111 | def netlocalgroup_redania_objects():
112 | log_file = os.path.join(TEST_DATA_DIR, "netlocalgroupbof_logs/netlocalgroupbof_redania.log")
113 | yield GenericParser.parse_file(log_file)
114 |
115 |
116 | # RegSession BOF Fixtures
117 | @pytest.fixture
118 | def regsession_redania_file():
119 | yield os.path.join(TEST_DATA_DIR, "regsessionbof_logs/regsessionbof_redania.log")
120 |
121 |
122 | @pytest.fixture
123 | def regsession_redania_objects():
124 | log_file = os.path.join(TEST_DATA_DIR, "regsessionbof_logs/regsessionbof_redania.log")
125 | yield GenericParser.parse_file(log_file)
126 |
127 |
128 | # fixture for processing marvel LDAP and local objects into a complete ADDS object
129 | @pytest.fixture
130 | def marvel_adds(testdata_marvel_ldap_objects, testdata_marvel_local_objects):
131 | ad = ADDS()
132 | broker = LocalBroker()
133 |
134 | ad.import_objects(testdata_marvel_ldap_objects)
135 | broker.import_objects(testdata_marvel_local_objects, ad.DOMAIN_MAP.values())
136 |
137 | ad.process()
138 | ad.process_local_objects(broker)
139 |
140 | yield ad
--------------------------------------------------------------------------------
/tests/test_data/json_output/computers_20220413_204956.json:
--------------------------------------------------------------------------------
1 | {"data": [{"LocalAdmins": {"Collected": false, "FailureReason": null, "Results": []}, "PSRemoteUsers": {"Collected": false, "FailureReason": null, "Results": []}, "RemoteDesktopUsers": {"Collected": false, "FailureReason": null, "Results": []}, "DcomUsers": {"Collected": false, "FailureReason": null, "Results": []}, "Sessions": {"Collected": false, "FailureReason": null, "Results": []}, "PrivilegedSessions": {"Collected": false, "FailureReason": null, "Results": []}, "RegistrySessions": {"Collected": false, "FailureReason": null, "Results": []}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "IsACLProtected": false, "ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-1000", "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-516", "AllowedToDelegate": [], "Properties": {"name": "DC1.EZ.LAB", "objectid": "S-1-5-21-3539700351-1165401899-3544196954-1000", "unconstraineddelegation": true, "enabled": true, "domain": "EZ.LAB", "highvalue": false, "distinguishedname": "CN=DC1,OU=Domain Controllers,DC=ez,DC=lab", "lastlogontimestamp": 1649365533, "lastlogon": 1649682341, "pwdlastset": 1648314470, "serviceprincipalnames": ["Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/DC1.ez.lab", "ldap/DC1.ez.lab/ForestDnsZones.ez.lab", "ldap/DC1.ez.lab/DomainDnsZones.ez.lab", "DNS/DC1.ez.lab", "GC/DC1.ez.lab/ez.lab", "RestrictedKrbHost/DC1.ez.lab", "RestrictedKrbHost/DC1", "RPC/6b8bdc96-cba0-48a5-88fd-e5c612da8ec0._msdcs.ez.lab", "HOST/DC1/EZ", "HOST/DC1.ez.lab/EZ", "HOST/DC1", "HOST/DC1.ez.lab", "HOST/DC1.ez.lab/ez.lab", "E3514235-4B06-11D1-AB04-00C04FC2DCD2/6b8bdc96-cba0-48a5-88fd-e5c612da8ec0/ez.lab", "ldap/DC1/EZ", "ldap/6b8bdc96-cba0-48a5-88fd-e5c612da8ec0._msdcs.ez.lab", "ldap/DC1.ez.lab/EZ", "ldap/DC1", "ldap/DC1.ez.lab", "ldap/DC1.ez.lab/ez.lab"], "operatingsystem": "Windows - 10.0 (17763)"}, "AllowedToAct": []}, {"LocalAdmins": {"Collected": false, "FailureReason": null, "Results": []}, "PSRemoteUsers": {"Collected": false, "FailureReason": null, "Results": []}, "RemoteDesktopUsers": {"Collected": false, "FailureReason": null, "Results": []}, "DcomUsers": {"Collected": false, "FailureReason": null, "Results": []}, "Sessions": {"Collected": false, "FailureReason": null, "Results": []}, "PrivilegedSessions": {"Collected": false, "FailureReason": null, "Results": []}, "RegistrySessions": {"Collected": false, "FailureReason": null, "Results": []}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "EZ.LAB-S-1-5-32-548", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "IsACLProtected": false, "ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-1105", "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-515", "AllowedToDelegate": [], "Properties": {"name": "WS1.EZ.LAB", "objectid": "S-1-5-21-3539700351-1165401899-3544196954-1105", "unconstraineddelegation": false, "enabled": true, "domain": "EZ.LAB", "highvalue": false, "distinguishedname": "CN=WS1,CN=Computers,DC=ez,DC=lab", "lastlogontimestamp": 1649082772, "lastlogon": 1649671471, "pwdlastset": 1647485202, "serviceprincipalnames": ["RestrictedKrbHost/WS1", "HOST/WS1", "RestrictedKrbHost/WS1.ez.lab", "HOST/WS1.ez.lab"], "operatingsystem": "Windows - 10.0 (19043)"}, "AllowedToAct": []}, {"LocalAdmins": {"Collected": false, "FailureReason": null, "Results": []}, "PSRemoteUsers": {"Collected": false, "FailureReason": null, "Results": []}, "RemoteDesktopUsers": {"Collected": false, "FailureReason": null, "Results": []}, "DcomUsers": {"Collected": false, "FailureReason": null, "Results": []}, "Sessions": {"Collected": false, "FailureReason": null, "Results": []}, "PrivilegedSessions": {"Collected": false, "FailureReason": null, "Results": []}, "RegistrySessions": {"Collected": false, "FailureReason": null, "Results": []}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "EZ.LAB-S-1-5-32-548", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "IsACLProtected": false, "ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-1106", "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-515", "AllowedToDelegate": [], "Properties": {"name": "WS2.EZ.LAB", "objectid": "S-1-5-21-3539700351-1165401899-3544196954-1106", "unconstraineddelegation": false, "enabled": true, "domain": "EZ.LAB", "highvalue": false, "distinguishedname": "CN=WS2,CN=Computers,DC=ez,DC=lab", "lastlogontimestamp": 1635250878, "lastlogon": 1635788537, "pwdlastset": 1635250885, "serviceprincipalnames": ["RestrictedKrbHost/WS2", "HOST/WS2", "RestrictedKrbHost/WS2.ez.lab", "HOST/WS2.ez.lab"], "operatingsystem": "Windows - 10.0 (19043)"}, "AllowedToAct": []}, {"LocalAdmins": {"Collected": false, "FailureReason": null, "Results": []}, "PSRemoteUsers": {"Collected": false, "FailureReason": null, "Results": []}, "RemoteDesktopUsers": {"Collected": false, "FailureReason": null, "Results": []}, "DcomUsers": {"Collected": false, "FailureReason": null, "Results": []}, "Sessions": {"Collected": false, "FailureReason": null, "Results": []}, "PrivilegedSessions": {"Collected": false, "FailureReason": null, "Results": []}, "RegistrySessions": {"Collected": false, "FailureReason": null, "Results": []}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "EZ.LAB-S-1-5-32-548", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "IsACLProtected": false, "ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-1112", "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-515", "AllowedToDelegate": [], "Properties": {"name": "TESTYBOI.EZ.LAB", "objectid": "S-1-5-21-3539700351-1165401899-3544196954-1112", "unconstraineddelegation": false, "enabled": true, "domain": "EZ.LAB", "highvalue": false, "distinguishedname": "CN=TestyBoi,CN=Computers,DC=ez,DC=lab", "lastlogontimestamp": 1639306889, "lastlogon": 1639307367, "pwdlastset": 1639306865}, "AllowedToAct": []}], "meta": {"type": "computers", "count": 4, "methods": 0, "version": 4}}
--------------------------------------------------------------------------------
/tests/test_data/json_output/domains_20220413_204956.json:
--------------------------------------------------------------------------------
1 | {"data": [{"ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954", "Properties": {"name": "EZ.LAB", "domain": "EZ.LAB", "objectid": "S-1-5-21-3539700351-1165401899-3544196954", "distinguishedname": "DC=EZ,DC=LAB", "highvalue": true, "functionallevel": "2016"}, "Trusts": [], "Aces": [{"RightName": "Owns", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GetChanges", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-1111", "IsInherited": false, "PrincipalType": "Unknown"}, {"RightName": "GetChanges", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-498", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GetChangesAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-516", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GetChangesAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-1111", "IsInherited": false, "PrincipalType": "Unknown"}, {"RightName": "GetChanges", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GetChangesAll", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GetChanges", "PrincipalSID": "EZ.LAB-S-1-5-9", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}], "Links": [], "ChildObjects": [], "GPOChanges": {"AffectedComputers": [], "DcomUsers": [], "LocalAdmins": [], "PSRemoteUsers": [], "RemoteDesktopUsers": []}, "IsDeleted": false, "IsACLProtected": false}], "meta": {"type": "domains", "count": 1, "methods": 0, "version": 4}}
--------------------------------------------------------------------------------
/tests/test_data/json_output/users_20220413_204956.json:
--------------------------------------------------------------------------------
1 | {"data": [{"ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-500", "AllowedToDelegate": [], "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-513", "Properties": {"domainsid": "S-1-5-21-3539700351-1165401899-3544196954", "name": "ADMINISTRATOR@EZ.LAB", "domain": "EZ.LAB", "distinguishedname": "CN=ADMINISTRATOR,CN=USERS,DC=EZ,DC=LAB", "admincount": true, "unconstraineddelegation": false, "passwordnotreqd": false, "enabled": true, "lastlogon": 1649638800, "lastlogontimestamp": 1648995141, "pwdlastset": 1639145510, "dontreqpreauth": false, "pwdneverexpires": true, "sensitive": false, "serviceprincipalnames": [], "description": "Built-in account for administering the computer/domain"}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}], "SPNTargets": [], "HasSIDHistory": [], "IsACLProtected": true}, {"ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-501", "AllowedToDelegate": [], "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-514", "Properties": {"domainsid": "S-1-5-21-3539700351-1165401899-3544196954", "name": "GUEST@EZ.LAB", "domain": "EZ.LAB", "distinguishedname": "CN=GUEST,CN=USERS,DC=EZ,DC=LAB", "unconstraineddelegation": false, "passwordnotreqd": true, "enabled": false, "lastlogon": 0, "pwdlastset": 0, "dontreqpreauth": false, "pwdneverexpires": true, "sensitive": false, "serviceprincipalnames": [], "description": "Built-in account for guest access to the computer/domain"}, "Aces": [{"RightName": "Owns", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "EZ.LAB-S-1-5-32-548", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "SPNTargets": [], "HasSIDHistory": [], "IsACLProtected": false}, {"ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-502", "AllowedToDelegate": [], "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-513", "Properties": {"domainsid": "S-1-5-21-3539700351-1165401899-3544196954", "name": "KRBTGT@EZ.LAB", "domain": "EZ.LAB", "distinguishedname": "CN=KRBTGT,CN=USERS,DC=EZ,DC=LAB", "admincount": true, "unconstraineddelegation": false, "passwordnotreqd": false, "enabled": false, "lastlogon": 0, "pwdlastset": 1630000541, "dontreqpreauth": false, "pwdneverexpires": false, "sensitive": false, "serviceprincipalnames": ["kadmin/changepw"], "hasspn": true, "description": "Key Distribution Center Service Account"}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": false, "PrincipalType": "Group"}], "SPNTargets": [], "HasSIDHistory": [], "IsACLProtected": true}, {"ObjectIdentifier": "S-1-5-21-3539700351-1165401899-3544196954-1110", "AllowedToDelegate": [], "PrimaryGroupSID": "S-1-5-21-3539700351-1165401899-3544196954-513", "Properties": {"domainsid": "S-1-5-21-3539700351-1165401899-3544196954", "name": "MATT@EZ.LAB", "domain": "EZ.LAB", "distinguishedname": "CN=MATT,CN=USERS,DC=EZ,DC=LAB", "unconstraineddelegation": false, "passwordnotreqd": false, "enabled": true, "lastlogon": 1649636900, "lastlogontimestamp": 1649082778, "pwdlastset": 1631290444, "dontreqpreauth": false, "pwdneverexpires": true, "sensitive": false, "serviceprincipalnames": [], "displayname": "Matt"}, "Aces": [{"RightName": "Owns", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-512", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "EZ.LAB-S-1-5-32-548", "IsInherited": false, "PrincipalType": "Group"}, {"RightName": "GenericAll", "PrincipalSID": "S-1-5-21-3539700351-1165401899-3544196954-519", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "GenericWrite", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteOwner", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "AllExtendedRights", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}, {"RightName": "WriteDacl", "PrincipalSID": "EZ.LAB-S-1-5-32-544", "IsInherited": true, "PrincipalType": "Group"}], "SPNTargets": [], "HasSIDHistory": [], "IsACLProtected": false}], "meta": {"type": "users", "count": 4, "methods": 0, "version": 4}}
--------------------------------------------------------------------------------
/tests/test_data/ldapsearchpy_logs/ldapsearch_20220414.log:
--------------------------------------------------------------------------------
1 | --------------------
2 | name: {31B2F340-016D-11D2-945F-00C04FB984F9}
3 | objectGUID: 9a44995b-34f9-4252-b113-fa2b297e5b40
4 | --------------------
5 | name: {6AC1786C-016F-11D2-945F-00C04fB984F9}
6 | objectGUID: f5a658e7-82d6-43a7-bef3-177f34c56200
7 | --------------------
8 | name: {79081BF9-A672-4475-A982-D622CC600A49}
9 | objectGUID: 1fbda1a0-7037-4c1a-8fef-5e336dc44fe5
10 | --------------------
11 | name: {F08300B1-8BFF-4866-8524-14C03B50D991}
12 | objectGUID: 0ccef504-dce6-47ec-8021-69bc972cff27
13 | --------------------
14 | cn: {31B2F340-016D-11D2-945F-00C04FB984F9}
15 | dSCorePropagationData: 20210826175542.0Z
16 | displayName: Default Domain Policy
17 | distinguishedName: CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=ez,DC=lab
18 | flags: 0
19 | gPCFileSysPath: \\ez.lab\sysvol\ez.lab\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}
20 | gPCFunctionalityVersion: 2
21 | gPCMachineExtensionNames: [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}][{827D319E-6EAC-11D2-A4EA-00C04F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}][{B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}][{F3CCC681-B74C-4060-9F26-CD84525DCA2A}{0F3F3735-573D-9804-99E4-AB2A69BA5FD4}]
22 | instanceType: 4
23 | isCriticalSystemObject: True
24 | nTSecurityDescriptor: AQAEnEgBAABkAQAAAAAAABQAAAAEADQBCgAAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAKJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAKJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAKFAD/AA8AAQEAAAAAAAMAAAAAAAIUAP8ADwABAQAAAAAABRIAAAAAAhQAlAACAAEBAAAAAAAFCwAAAAUCKAAAAQAAAQAAAI/9rO2z/9ERtB0AoMlo+TkBAQAAAAAABQsAAAAAAhQAlAACAAEBAAAAAAAFCQAAAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAA
25 | name: {31B2F340-016D-11D2-945F-00C04FB984F9}
26 | objectCategory: CN=Group-Policy-Container,CN=Schema,CN=Configuration,DC=ez,DC=lab
27 | objectClass: top, container, groupPolicyContainer
28 | objectGUID: 9a44995b-34f9-4252-b113-fa2b297e5b40
29 | showInAdvancedViewOnly: True
30 | systemFlags: -1946157056
31 | uSNChanged: 52468
32 | uSNCreated: 5672
33 | versionNumber: 8
34 | whenChanged: 20211213123711.0Z
35 | whenCreated: 20210826173041.0Z
36 | --------------------
37 | cn: {6AC1786C-016F-11D2-945F-00C04fB984F9}
38 | dSCorePropagationData: 20210826175542.0Z
39 | displayName: Default Domain Controllers Policy
40 | distinguishedName: CN={6AC1786C-016F-11D2-945F-00C04fB984F9},CN=Policies,CN=System,DC=ez,DC=lab
41 | flags: 0
42 | gPCFileSysPath: \\ez.lab\sysvol\ez.lab\Policies\{6AC1786C-016F-11D2-945F-00C04fB984F9}
43 | gPCFunctionalityVersion: 2
44 | gPCMachineExtensionNames: [{827D319E-6EAC-11D2-A4EA-00C04F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}]
45 | instanceType: 4
46 | isCriticalSystemObject: True
47 | nTSecurityDescriptor: AQAEnEgBAABkAQAAAAAAABQAAAAEADQBCgAAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAKJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAKJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAAAJAC9AA4AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAAKFAD/AA8AAQEAAAAAAAMAAAAAAAIUAP8ADwABAQAAAAAABRIAAAAAAhQAlAACAAEBAAAAAAAFCwAAAAUCKAAAAQAAAQAAAI/9rO2z/9ERtB0AoMlo+TkBAQAAAAAABQsAAAAAAhQAlAACAAEBAAAAAAAFCQAAAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAA
48 | name: {6AC1786C-016F-11D2-945F-00C04fB984F9}
49 | objectCategory: CN=Group-Policy-Container,CN=Schema,CN=Configuration,DC=ez,DC=lab
50 | objectClass: top, container, groupPolicyContainer
51 | objectGUID: f5a658e7-82d6-43a7-bef3-177f34c56200
52 | showInAdvancedViewOnly: True
53 | systemFlags: -1946157056
54 | uSNChanged: 42040
55 | uSNCreated: 5675
56 | versionNumber: 7
57 | whenChanged: 20210910171933.0Z
58 | whenCreated: 20210826173041.0Z
59 | --------------------
60 | cn: {79081BF9-A672-4475-A982-D622CC600A49}
61 | dSCorePropagationData: 20210901200136.0Z
62 | displayName: SMB File-Print and WMI
63 | distinguishedName: CN={79081BF9-A672-4475-A982-D622CC600A49},CN=Policies,CN=System,DC=ez,DC=lab
64 | flags: 0
65 | gPCFileSysPath: \\ez.lab\SysVol\ez.lab\Policies\{79081BF9-A672-4475-A982-D622CC600A49}
66 | gPCFunctionalityVersion: 2
67 | gPCMachineExtensionNames: [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{D02B1F72-3407-48AE-BA88-E8213C6761F1}]
68 | instanceType: 4
69 | nTSecurityDescriptor: AQAEnFwBAAB4AQAAAAAAABQAAAAEAEgBCgAAAAUCOAAAAQAAAQAAAI/9rO2z/9ERtB0AoMlo+TkBBQAAAAAABRUAAAB/ivvSK592RVonQNMDAgAABQIoAAABAAABAAAAj/2s7bP/0RG0HQCgyWj5OQEBAAAAAAAFCwAAAAAAJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAACJAAUAAIAAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAwIAAAACJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAACJAD/AA8AAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTBwIAAAACFACUAAIAAQEAAAAAAAUJAAAAAAIUAJQAAgABAQAAAAAABQsAAAAAAhQA/wAPAAEBAAAAAAAFEgAAAAAKFAD/AA8AAQEAAAAAAAMAAAAAAQUAAAAAAAUVAAAAf4r70iufdkVaJ0DTAAIAAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAA=
70 | name: {79081BF9-A672-4475-A982-D622CC600A49}
71 | objectCategory: CN=Group-Policy-Container,CN=Schema,CN=Configuration,DC=ez,DC=lab
72 | objectClass: top, container, groupPolicyContainer
73 | objectGUID: 1fbda1a0-7037-4c1a-8fef-5e336dc44fe5
74 | showInAdvancedViewOnly: True
75 | uSNChanged: 33064
76 | uSNCreated: 32876
77 | versionNumber: 4
78 | whenChanged: 20210901200136.0Z
79 | whenCreated: 20210831134847.0Z
80 | --------------------
81 | cn: {F08300B1-8BFF-4866-8524-14C03B50D991}
82 | dSCorePropagationData: 16010101000000.0Z
83 | displayName: Disable Defender
84 | distinguishedName: CN={F08300B1-8BFF-4866-8524-14C03B50D991},CN=Policies,CN=System,DC=ez,DC=lab
85 | flags: 0
86 | gPCFileSysPath: \\ez.lab\SysVol\ez.lab\Policies\{F08300B1-8BFF-4866-8524-14C03B50D991}
87 | gPCFunctionalityVersion: 2
88 | gPCMachineExtensionNames: [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{D02B1F72-3407-48AE-BA88-E8213C6761F1}]
89 | instanceType: 4
90 | nTSecurityDescriptor: AQAEnAABAAAcAQAAAAAAABQAAAAEAOwACAAAAAUCKAAAAQAAAQAAAI/9rO2z/9ERtB0AoMlo+TkBAQAAAAAABQsAAAAAACQA/wAPAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAAAAiQA/wAPAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAAAAiQA/wAPAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wcCAAAAAhQAlAACAAEBAAAAAAAFCQAAAAACFACUAAIAAQEAAAAAAAULAAAAAAIUAP8ADwABAQAAAAAABRIAAAAAChQA/wAPAAEBAAAAAAADAAAAAAEFAAAAAAAFFQAAAH+K+9Irn3ZFWidA0wACAAABBQAAAAAABRUAAAB/ivvSK592RVonQNMAAgAA
91 | name: {F08300B1-8BFF-4866-8524-14C03B50D991}
92 | objectCategory: CN=Group-Policy-Container,CN=Schema,CN=Configuration,DC=ez,DC=lab
93 | objectClass: top, container, groupPolicyContainer
94 | objectGUID: 0ccef504-dce6-47ec-8021-69bc972cff27
95 | showInAdvancedViewOnly: True
96 | uSNChanged: 42050
97 | uSNCreated: 36992
98 | versionNumber: 23
99 | whenChanged: 20210910172157.0Z
100 | whenCreated: 20210901233038.0Z
101 | --------------------
102 | displayName: Default Domain Policy
103 | objectGUID: 9a44995b-34f9-4252-b113-fa2b297e5b40
104 | --------------------
105 | displayName: Default Domain Controllers Policy
106 | objectGUID: f5a658e7-82d6-43a7-bef3-177f34c56200
107 | --------------------
108 | displayName: SMB File-Print and WMI
109 | objectGUID: 1fbda1a0-7037-4c1a-8fef-5e336dc44fe5
110 | --------------------
111 | displayName: Disable Defender
112 | objectGUID: 0ccef504-dce6-47ec-8021-69bc972cff27
113 |
--------------------------------------------------------------------------------
/tests/test_data/netlocalgroupbof_logs/netlocalgroupbof_redania.log:
--------------------------------------------------------------------------------
1 | Querying Remote Desktop Users...
2 | ----------Local Group Member----------
3 | Host: oxenfurt.redania.local
4 | Group: Remote Desktop Users
5 | Member: REDANIA\Domain Admins
6 | MemberSid: S-1-5-21-1308756548-3893869957-2915408613-512
7 | MemberSidType: Group
8 | --------End Local Group Member--------
9 |
10 | Querying Distributed COM Users...
11 | ----------Local Group Member----------
12 | Host: oxenfurt.redania.local
13 | Group: Distributed COM Users
14 | Member: REDANIA\geralt
15 | MemberSid: S-1-5-21-1308756548-3893869957-2915408613-1103
16 | MemberSidType: User
17 | --------End Local Group Member--------
18 |
19 | Querying Remote Management Users...
20 | Querying Administrators...
21 | ----------Local Group Member----------
22 | Host: oxenfurt.redania.local
23 | Group: Administrators
24 | Member: OXENFURT\Administrator
25 | MemberSid: S-1-5-21-3209726975-3062735514-2329926824-500
26 | MemberSidType: User
27 | --------End Local Group Member--------
28 |
29 | ----------Local Group Member----------
30 | Host: oxenfurt.redania.local
31 | Group: Administrators
32 | Member: REDANIA\Domain Admins
33 | MemberSid: S-1-5-21-1308756548-3893869957-2915408613-512
34 | MemberSidType: Group
35 | --------End Local Group Member--------
36 |
37 | ----------Local Group Member----------
38 | Host: oxenfurt.redania.local
39 | Group: Administrators
40 | Member: OXENFURT\localadmin
41 | MemberSid: S-1-5-21-3209726975-3062735514-2329926824-1001
42 | MemberSidType: User
43 | --------End Local Group Member--------
--------------------------------------------------------------------------------
/tests/test_data/netloggedonbof_logs/netloggedonbof_redania.log:
--------------------------------------------------------------------------------
1 | -----------Logged on User-----------
2 | Host: Oxenfurt.redania.local
3 | Username: localadmin
4 | Domain: OXENFURT
5 | Oth_domains:
6 | Logon server: OXENFURT
7 | ---------End Logged on User---------
8 |
9 | -----------Logged on User-----------
10 | Host: Oxenfurt.redania.local
11 | Username: localadmin
12 | Domain: OXENFURT
13 | Oth_domains:
14 | Logon server: OXENFURT
15 | ---------End Logged on User---------
16 |
17 | -----------Logged on User-----------
18 | Host: Oxenfurt.redania.local
19 | Username: OXENFURT$
20 | Domain: REDANIA
21 | Oth_domains:
22 | Logon server:
23 | ---------End Logged on User---------
24 |
25 | -----------Logged on User-----------
26 | Host: Oxenfurt.redania.local
27 | Username: OXENFURT$
28 | Domain: REDANIA
29 | Oth_domains:
30 | Logon server:
31 | ---------End Logged on User---------
32 |
33 | -----------Logged on User-----------
34 | Host: Oxenfurt.redania.local
35 | Username: OXENFURT$
36 | Domain: REDANIA
37 | Oth_domains:
38 | Logon server:
39 | ---------End Logged on User---------
40 |
41 | -----------Logged on User-----------
42 | Host: Oxenfurt.redania.local
43 | Username: sqlsvc
44 | Domain: REDANIA
45 | Oth_domains:
46 | Logon server: TRETOGOR
47 | ---------End Logged on User---------
48 |
49 | -----------Logged on User-----------
50 | Host: Oxenfurt.redania.local
51 | Username: sqlsvc
52 | Domain: REDANIA
53 | Oth_domains:
54 | Logon server: TRETOGOR
55 | ---------End Logged on User---------
56 |
57 | -----------Logged on User-----------
58 | Host: Oxenfurt.redania.local
59 | Username: sqlsvc
60 | Domain: REDANIA
61 | Oth_domains:
62 | Logon server: TRETOGOR
63 | ---------End Logged on User---------
64 |
65 | -----------Logged on User-----------
66 | Host: Oxenfurt.redania.local
67 | Username: sqlsvc
68 | Domain: REDANIA
69 | Oth_domains:
70 | Logon server: TRETOGOR
71 | ---------End Logged on User---------
72 |
73 | -----------Logged on User-----------
74 | Host: Oxenfurt.redania.local
75 | Username: OXENFURT$
76 | Domain: REDANIA
77 | Oth_domains:
78 | Logon server:
79 | ---------End Logged on User---------
80 |
81 | -----------Logged on User-----------
82 | Host: Oxenfurt.redania.local
83 | Username: OXENFURT$
84 | Domain: REDANIA
85 | Oth_domains:
86 | Logon server:
87 | ---------End Logged on User---------
88 |
89 | -----------Logged on User-----------
90 | Host: Oxenfurt.redania.local
91 | Username: OXENFURT$
92 | Domain: REDANIA
93 | Oth_domains:
94 | Logon server:
95 | ---------End Logged on User---------
--------------------------------------------------------------------------------
/tests/test_data/netsessionbof_logs/netsessionbof_redania_dns.log:
--------------------------------------------------------------------------------
1 | ---------------Session--------------
2 | Client: \\192.168.0.235
3 | PTR: tretogor.redania.local
4 | User: Administrator
5 | Active: 0
6 | Idle: 0
7 | -------------End Session------------
8 |
9 | ---------------Session--------------
10 | Client: \\192.168.0.235
11 | PTR: No PTR record found; reverse lookup failed
12 | User: Administrator
13 | Active: 0
14 | Idle: 0
15 | -------------End Session------------
--------------------------------------------------------------------------------
/tests/test_data/netsessionbof_logs/netsessionbof_redania_netapi.log:
--------------------------------------------------------------------------------
1 | [*] Enumerating sessions for system: oxenfurt.redania.local
2 | [*] Resolving client IPs to hostnames using NetWkstaGetInfo
3 |
4 | ---------------Session--------------
5 | Client: \\192.168.0.235
6 | ComputerName: TRETOGOR
7 | ComputerDomain: REDANIA
8 | User: Administrator
9 | Active: 0
10 | Idle: 0
11 | -------------End Session------------
12 |
13 | ---------------Session--------------
14 | Client: \\192.168.0.200
15 | ComputerName: NetWkstaGetInfo Failed; 53
16 | ComputerDomain: NetWkstaGetInfo Failed; 53
17 | User: Administrator
18 | Active: 0
19 | Idle: 0
20 | -------------End Session------------
--------------------------------------------------------------------------------
/tests/test_data/regsessionbof_logs/regsessionbof_redania.log:
--------------------------------------------------------------------------------
1 | [*] Querying local registry...
2 | -----------Registry Session---------
3 | UserSid: S-1-5-21-1308756548-3893869957-2915408613-1116
4 | Host: TRETOGOR.redania.local
5 | ---------End Registry Session-------
6 |
7 | -----------Registry Session---------
8 | UserSid: S-1-5-21-1308756548-3893869957-2915408613-500
9 | Host: TRETOGOR.redania.local
10 | ---------End Registry Session-------
11 |
12 | [*] Found 2 sessions in the registry
13 |
14 | [*] Querying registry on oxenfurt.redania.local...
15 | -----------Registry Session---------
16 | UserSid: S-1-5-21-1308756548-3893869957-2915408613-1116
17 | Host: oxenfurt.redania.local
18 | ---------End Registry Session-------
19 |
20 | -----------Registry Session---------
21 | UserSid: S-1-5-21-3209726975-3062735514-2329926824-1001
22 | Host: oxenfurt.redania.local
23 | ---------End Registry Session-------
24 |
25 | [*] Found 2 sessions in the registry
26 |
--------------------------------------------------------------------------------
/tests/test_localgroup_to_computer.py:
--------------------------------------------------------------------------------
1 | from tests.test_data import *
2 |
3 | EARTH_DC_SID = "S-1-5-21-3719975868-1113416855-2416171545-1000"
4 | ASGARD_WKSTN_SID = "S-1-5-21-3719975868-1113416855-2416171545-1154"
5 |
6 |
7 | def test_earth_dc_group_counts(marvel_adds):
8 | earth_dc_local_groups = marvel_adds.SID_MAP[EARTH_DC_SID].local_group_members
9 |
10 | assert len(earth_dc_local_groups) == 2
11 | assert "remote desktop users" not in earth_dc_local_groups.keys()
12 | assert "remote management users" not in earth_dc_local_groups.keys()
13 | assert len(earth_dc_local_groups["distributed com users"]) == 1
14 | assert len(earth_dc_local_groups["administrators"]) == 3
15 |
16 |
17 | def test_asgard_wrkstn_group_counts(marvel_adds):
18 | asgard_wrkstn_local_groups = marvel_adds.SID_MAP[ASGARD_WKSTN_SID].local_group_members
19 |
20 | assert len(asgard_wrkstn_local_groups) == 2
21 | assert "distributed com users" not in asgard_wrkstn_local_groups.keys()
22 | assert "remote management users" not in asgard_wrkstn_local_groups.keys()
23 | assert len(asgard_wrkstn_local_groups["remote desktop users"]) == 4
24 | assert len(asgard_wrkstn_local_groups["administrators"]) == 5
25 |
--------------------------------------------------------------------------------
/tests/test_session_to_computer.py:
--------------------------------------------------------------------------------
1 | from tests.test_data import *
2 |
3 | THOR_SID = "S-1-5-21-3719975868-1113416855-2416171545-1104"
4 | EARTH_DC_SID = "S-1-5-21-3719975868-1113416855-2416171545-1000"
5 | ASGARD_WKSTN_SID = "S-1-5-21-3719975868-1113416855-2416171545-1154"
6 |
7 |
8 | def test_marvel_privileged_sessions(marvel_adds):
9 | earth_dc_priv_sessions = marvel_adds.SID_MAP[EARTH_DC_SID].privileged_sessions
10 |
11 | assert len(earth_dc_priv_sessions) == 1
12 | assert earth_dc_priv_sessions[0]["UserSID"] == THOR_SID
13 |
14 |
15 |
16 | def test_marvel_sessions(marvel_adds):
17 | asgard_wrkstn_sessions = marvel_adds.SID_MAP[ASGARD_WKSTN_SID].sessions
18 | earth_dc_sessions = marvel_adds.SID_MAP[EARTH_DC_SID].sessions
19 |
20 | assert len(earth_dc_sessions) == 0
21 | assert len(asgard_wrkstn_sessions) == 1
22 | assert asgard_wrkstn_sessions[0]["UserSID"] == THOR_SID
23 |
24 |
25 |
26 | def test_marvel_registry_sessions(marvel_adds):
27 | earth_dc_reg_sessions = marvel_adds.SID_MAP[EARTH_DC_SID].registry_sessions
28 | asgard_wrkstn_reg_sessions = marvel_adds.SID_MAP[ASGARD_WKSTN_SID].registry_sessions
29 |
30 | assert len(earth_dc_reg_sessions) == 1
31 | assert len(asgard_wrkstn_reg_sessions) == 1
32 | assert earth_dc_reg_sessions[0]["UserSID"] == THOR_SID
33 | assert asgard_wrkstn_reg_sessions[0]["UserSID"] == THOR_SID
34 |
35 |
36 |
--------------------------------------------------------------------------------