├── C2_Profiles └── .keep ├── agent_icons ├── .keep └── bloodhound.svg ├── documentation-c2 └── .keep ├── documentation-payload ├── .keep └── bloodhound │ ├── commands │ ├── get_owned.md │ ├── cypher_list_saved.md │ ├── whoami.md │ ├── get_domains.md │ ├── search.md │ ├── graph_search.md │ ├── cypher_delete_saved.md │ ├── cypher_saved.md │ ├── get_user.md │ ├── get_group.md │ ├── get_object.md │ ├── cypher.md │ ├── upload_status.md │ ├── get_user_memberships.md │ ├── get_group_members.md │ ├── get_group_memberships.md │ ├── cypher_predefined.md │ ├── cypher_create_saved.md │ ├── upload.md │ ├── mark_owned.md │ ├── get_domains_foreign_users.md │ ├── controllables.md │ ├── shortest_path.md │ └── _index.md │ ├── contributing │ ├── _index.md │ └── Tasks.md │ └── _index.md ├── documentation-wrapper └── .keep ├── Payload_Type └── bloodhound │ ├── bloodhound │ ├── agent_functions │ │ ├── __init__.py │ │ ├── builder.py │ │ ├── whoami.py │ │ ├── get_domains.py │ │ ├── cypher_list_saved.py │ │ ├── get_owned.py │ │ ├── graph_search.py │ │ ├── get_object.py │ │ ├── get_group.py │ │ ├── get_group_members.py │ │ ├── get_user_memberships.py │ │ ├── get_group_memberships.py │ │ ├── cypher.py │ │ ├── get_domains_foreign_users.py │ │ ├── cypher_delete_saved.py │ │ ├── controllables.py │ │ ├── cypher_create_saved.py │ │ ├── upload_status.py │ │ ├── search.py │ │ ├── mark_owned.py │ │ ├── shortest_path.py │ │ ├── cypher_saved.py │ │ ├── get_user.py │ │ ├── upload.py │ │ ├── bloodhound.svg │ │ └── cypher_predefined.py │ ├── BloodhoundRequests │ │ ├── __init__.py │ │ ├── BloodhoundAPIClasses.py │ │ └── BloodhoundAPI.py │ ├── __init__.py │ └── browser_scripts │ │ ├── get_owned.js │ │ ├── upload_status.js │ │ ├── controllables.js │ │ ├── cypher_list_saved.js │ │ ├── search.js │ │ ├── get_group_members.js │ │ ├── graph_search.js │ │ └── cypher.js │ ├── .docker │ ├── requirements.txt │ └── Dockerfile │ ├── CHANGELOG.MD │ ├── main.py │ └── Dockerfile ├── config.json ├── agent_capabilities.json ├── LICENSE.md ├── .gitignore ├── README.md └── .github └── workflows └── docker.yml /C2_Profiles/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /agent_icons/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-c2/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-payload/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation-wrapper/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/BloodhoundRequests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/.docker/requirements.txt: -------------------------------------------------------------------------------- 1 | mythic-container==0.5.31 2 | requests -------------------------------------------------------------------------------- /Payload_Type/bloodhound/CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | ## [v0.0.2] - 2025-04-18 2 | 3 | ### Changed 4 | 5 | - Updated the pypi package and the predefined queries -------------------------------------------------------------------------------- /Payload_Type/bloodhound/main.py: -------------------------------------------------------------------------------- 1 | import mythic_container 2 | import asyncio 3 | # import the bloodhound agent 4 | import bloodhound 5 | 6 | mythic_container.mythic_service.start_and_run_forever() -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_payload_type": false, 3 | "exclude_c2_profiles": false, 4 | "exclude_documentation_payload": false, 5 | "exclude_documentation_c2": false, 6 | "exclude_agent_icons": false, 7 | "remote_images": { 8 | "bloodhound": "ghcr.io/mythicagents/bloodhound:v0.0.0.3" 9 | } 10 | } -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_owned.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_owned" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the entities marked as owned. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | ## Usage 16 | ``` 17 | get_owned 18 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher_list_saved.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher_list_saved" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | List out saved cypher queries. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | ## Usage 16 | ``` 17 | cypher_list_saved 18 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/whoami.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "whoami" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get information about the credentials used to authenticate to bloodhound. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | ## Usage 15 | ``` 16 | whoami 17 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/contributing/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Contributing" 3 | chapter = true 4 | weight = 20 5 | pre = "4. " 6 | +++ 7 | 8 | {{% notice info %}} 9 | Developer documentation is incomplete at this time. As such, your mileage may vary with the following contents. 10 | {{% /notice %}} 11 | 12 | {{% children %}} -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_domains.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_domains" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | List the available domains and some basic information about them. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | ## Usage 16 | ``` 17 | get_domains 18 | ``` -------------------------------------------------------------------------------- /agent_capabilities.json: -------------------------------------------------------------------------------- 1 | { 2 | "os": ["Windows", "Linux", "macOS"], 3 | "languages": ["python"] , 4 | "features": { 5 | "mythic": ["browser scripts", "docker"], 6 | "custom": [] 7 | }, 8 | "payload_output": [], 9 | "architectures": [], 10 | "c2": [], 11 | "mythic_version": "3.3.1-rc64", 12 | "agent_version": "0.0.3", 13 | "supported_wrappers": [] 14 | } 15 | -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/search.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "search" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Search the graph database for a specific entity. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query 16 | The object to search for 17 | 18 | ## Usage 19 | ``` 20 | search -query admin 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/graph_search.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "graph_search" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Search the graph database for a specific entity. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query 16 | The object to search for 17 | 18 | ## Usage 19 | ``` 20 | graph_search -query admin 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher_delete_saved.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher_delete_saved" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Delete a new saved cypher query. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query_id 16 | The ID for the saved query 17 | 18 | 19 | ## Usage 20 | ``` 21 | cypher_delete_saved -query_id 10 22 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher_saved.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher_saved" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Run a cypher query you saved by name. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query_name 16 | The name of the saved query you want to execute 17 | 18 | 19 | ## Usage 20 | ``` 21 | cypher_saved -query_name "All Domain Admins" 22 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_user.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_user" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get information about a specific user by object id. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the user to query 17 | 18 | ## Usage 19 | ``` 20 | get_user -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_group.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_group" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get information about a specific group by object id. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the group to query 17 | 18 | ## Usage 19 | ``` 20 | get_group -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_object.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_object" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get information about a specific object by object id. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the object to query 17 | 18 | ## Usage 19 | ``` 20 | get_object -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Execute an arbitrary Cypher query within bloodhound and view the results as a graph. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query 16 | The Cypher query to execute 17 | 18 | ## Usage 19 | ``` 20 | cypher -query \"MATCH (n:User)WHERE n.hasspn=true RETURN n\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/upload_status.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "upload_status" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get processing status for files uploaded to bloodhound. Get status for the latest 10 uploads or by specific upload id. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### id 16 | Id for a specific upload to check on. 17 | 18 | ## Usage 19 | ``` 20 | upload_status 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_user_memberships.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_user_memberships" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the groups that this user belongs to. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the user to query 17 | 18 | ## Usage 19 | ``` 20 | get_user_memberships -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_group_members.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_group_members" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the groups/users that belong to a specific group. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the group to query 17 | 18 | ## Usage 19 | ``` 20 | get_group_members -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_group_memberships.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_group_memberships" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the groups that this group belongs to. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the group to query 17 | 18 | ## Usage 19 | ``` 20 | get_group_memberships -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher_predefined.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher_predefined" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Run a pre-defined query from Bloodhound by name. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query_name 16 | The name of the query within Bloodhound's UI that you want to execute 17 | 18 | 19 | ## Usage 20 | ``` 21 | cypher_predefined -query_name "All Domain Admins" 22 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/cypher_create_saved.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "cypher_create_saved" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Create a new saved cypher query. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### query 16 | The Cypher query to save 17 | 18 | #### name 19 | The name to use for the cypher query 20 | 21 | ## Usage 22 | ``` 23 | cypher_create_saved -query \"MATCH (n:User)WHERE n.hasspn=true RETURN n\" -name "my special query" 24 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/upload.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "upload" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Upload a file to the bloodhound server for processing. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### file 16 | An uploaded new file to submit to bloodhound. 17 | 18 | #### filename 19 | The name of a file that Mythic has already registered from an agent as part of download. 20 | 21 | ## Usage 22 | ``` 23 | upload -filename compuers.json 24 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/mark_owned.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "mark_owned" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Mark an object as owned or remove the owned tag from an object by ID. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the object to add "owned" or remove "owned" from. 17 | 18 | #### removed 19 | True if you want to remove owned instead of add it. 20 | 21 | ## Usage 22 | ``` 23 | mark_owned -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 24 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/get_domains_foreign_users.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "get_domains_foreign_users" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the list of foreign users that are members of groups within the specified domain. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the domain to query to see if it has groups with user members that belong to other domains 17 | 18 | ## Usage 19 | ``` 20 | get_domains_foreign_users -object_id \"S-1-5-21-909015691-3030120388-2582151266\" 21 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/controllables.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "controllables" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the outbound control the specified object has over other objects. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### object_id 16 | The object_id for the object to inspect - this will typically be in the form `S-1-5-21-909015691-3030120388-2582151266-512`, but not always. 17 | You can use the `search` feature to look for specific objects by name and get the object id from them. 18 | 19 | ## Usage 20 | ``` 21 | controllables -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\" 22 | ``` -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/shortest_path.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "shortest_path" 3 | chapter = false 4 | weight = 103 5 | hidden = false 6 | +++ 7 | 8 | ## Summary 9 | 10 | Get the shortest path between two objects and view the response as a graph.. 11 | 12 | ### Arguments (Positional or Popup) 13 | 14 | 15 | #### start_node 16 | The object_id for the starting node 17 | 18 | #### end_node 19 | The object_id for the ending node 20 | 21 | #### relationships 22 | What kinds of edges to support in the path searching algorithm. 23 | 24 | ## Usage 25 | ``` 26 | shortest_path -start_node \"S-1-5-21-909015691-3030120388-2582151266\" -end_node \"S-1-5-21-909015691-3030120388-2582151739\" 27 | ``` -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/__init__.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os.path 3 | from pathlib import Path 4 | from importlib import import_module, invalidate_caches 5 | import sys 6 | # Get file paths of all modules. 7 | 8 | currentPath = Path(__file__) 9 | searchPath = currentPath.parent / "agent_functions" / "*.py" 10 | modules = glob.glob(f"{searchPath}") 11 | invalidate_caches() 12 | for x in modules: 13 | if not x.endswith("__init__.py") and x[-3:] == ".py": 14 | module = import_module(f"{__name__}.agent_functions." + Path(x).stem) 15 | for el in dir(module): 16 | if "__" not in el: 17 | globals()[el] = getattr(module, el) 18 | 19 | 20 | sys.path.append(os.path.abspath(currentPath.name)) 21 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /Payload_Type/bloodhound/.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim-bookworm as builder 2 | 3 | COPY [".docker/requirements.txt", "requirements.txt"] 4 | RUN apt-get -y update && \ 5 | apt-get -y upgrade && \ 6 | apt-get install --no-install-recommends \ 7 | software-properties-common apt-utils make build-essential libssl-dev zlib1g-dev libbz2-dev \ 8 | xz-utils tk-dev libffi-dev liblzma-dev libsqlite3-dev protobuf-compiler \ 9 | binutils-aarch64-linux-gnu libc-dev-arm64-cross -y 10 | RUN python3 -m pip wheel --wheel-dir /wheels -r requirements.txt 11 | 12 | FROM python:3.11-slim-bookworm 13 | 14 | COPY --from=builder /wheels /wheels 15 | 16 | RUN pip install --no-cache /wheels/* 17 | 18 | WORKDIR /Mythic/ 19 | 20 | COPY [".", "."] 21 | 22 | CMD ["python3", "main.py"] -------------------------------------------------------------------------------- /documentation-payload/bloodhound/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Bloodhound" 3 | chapter = true 4 | weight = 100 5 | +++ 6 | 7 | ![logo](/agents/bloodhound/bloodhound.svg?width=100px) 8 | 9 | ## Summary 10 | 11 | Bloodhound is a service agent for Mythic - it doesn't generate any payloads, but instead provides an easy interface to work with an external instance of Bloodhound Community Edition. 12 | 13 | {{% notice info %}} 14 | You must generate an API Token and ID in Bloodhound to use this agent. Once you've generated those in bloodhound, go to your user settings in Mythic and click the "red key" icon to configure your secrets. 15 | 16 | BLOODHOUND_API_KEY and BLOODHOUND_API_ID are the two secret keys to configure. 17 | {{% /notice %}} 18 | 19 | ### Highlighted Agent Features 20 | 21 | - File Uploads to Bloodhound 22 | - Cypher Queries (custom, saved, and all pre-defined ones in Bloodhound's UI) 23 | - Graph Views 24 | - Mark as Owned 25 | 26 | ## Authors 27 | 28 | - [@its_a_feature_](https://twitter.com/its_a_feature_) 29 | 30 | ### Special Thanks to These Contributors 31 | 32 | - Bloodhound Team for their API and amazing service 33 | 34 | ## Table of Contents 35 | 36 | {{% children %}} -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) 2024, its-a-feature 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | * Neither the name of bloodhound, mythic, nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /documentation-payload/bloodhound/commands/_index.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Commands" 3 | chapter = true 4 | weight = 15 5 | pre = "2. " 6 | +++ 7 | 8 | ![logo](/bloodhound/bloodhound/bloodhound.svg?width=100px) 9 | 10 | ## Table of Contents 11 | 12 | - Cypher 13 | * [cypher](/agents/bloodhound/commands/cypher/) 14 | * [cypher_create_saved](/agents/bloodhound/commands/cypher_create_saved/) 15 | * [cypher_delete_saved](/agents/bloodhound/commands/cypher_delete_saved/) 16 | * [cypher_list_saved](/agents/bloodhound/commands/cypher_list_saved/) 17 | * [cypher_predefined](/agents/bloodhound/commands/cypher_predefined/) 18 | * [cypher_saved](/agents/bloodhound/commands/cypher_saved/) 19 | * [shortest_path](/agents/bloodhound/commands/shortest_path/) 20 | - Misc 21 | * [controllables](/agents/bloodhound/commands/controllables/) 22 | * [get_object](/agents/bloodhound/commands/get_object/) 23 | - Domains 24 | * [get_domains](/agents/bloodhound/commands/get_domains/) 25 | * [get_domains_foreign_users](/agents/bloodhound/commands/get_domains_foreign_users/) 26 | - Groups 27 | * [get_group](/agents/bloodhound/commands/get_group/) 28 | * [get_group_members](/agents/bloodhound/commands/get_group_members/) 29 | * [get_group_memberships](/agents/bloodhound/commands/get_group_memberships/) 30 | - Owned 31 | * [get_owned](/agents/bloodhound/commands/get_owned/) 32 | * [mark_owned](/agents/bloodhound/commands/mark_owned/) 33 | - Users 34 | * [get_user](/agents/bloodhound/commands/get_user/) 35 | * [get_user_memberships](/agents/bloodhound/commands/get_user_memberships/) 36 | - Search 37 | * [graph_search](/agents/bloodhound/commands/graph_search/) 38 | * [search](/agents/bloodhound/commands/search/) 39 | - Upload 40 | * [upload](/agents/bloodhound/commands/upload/) 41 | * [upload_status](/agents/bloodhound/commands/upload_status/) -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/builder.py: -------------------------------------------------------------------------------- 1 | import mythic_container.PayloadBuilder 2 | from mythic_container.PayloadBuilder import * 3 | from mythic_container.MythicCommandBase import * 4 | from mythic_container.MythicRPC import * 5 | 6 | Version = "0.0.3" 7 | 8 | 9 | class Bloodhound(PayloadType): 10 | name = "bloodhound" 11 | file_extension = "" 12 | author = "@its_a_feature_" 13 | supported_os = [ 14 | SupportedOS("bloodhound") 15 | ] 16 | wrapper = False 17 | wrapped_payloads = [] 18 | note = f""" 19 | This payload, v{Version} communicates with an existing Bloodhound Community Edition instance. 20 | Use BLOODHOUND_API_ID and BLOODHOUND_API_KEY in your user secrets for connectivity. 21 | """ 22 | supports_dynamic_loading = False 23 | mythic_encrypts = True 24 | translation_container = None 25 | agent_type = AgentType.Service # AgentType.Agent 26 | agent_path = pathlib.Path(".") / "bloodhound" 27 | agent_icon_path = agent_path / "agent_functions" / "bloodhound.svg" 28 | agent_code_path = agent_path / "agent_code" 29 | build_parameters = [ 30 | BuildParameter(name="URL", 31 | description="Bloodhound URL", 32 | parameter_type=BuildParameterType.String, 33 | default_value="https://127.0.0.1:8080"), 34 | ] 35 | c2_profiles = [] 36 | 37 | async def build(self) -> BuildResponse: 38 | # this function gets called to create an instance of your payload 39 | resp = BuildResponse(status=BuildStatus.Success) 40 | ip = "127.0.0.1" 41 | create_callback = await SendMythicRPCCallbackCreate(MythicRPCCallbackCreateMessage( 42 | PayloadUUID=self.uuid, 43 | C2ProfileName="", 44 | User="Bloodhound", 45 | Host="Bloodhound", 46 | Ip=ip, 47 | IntegrityLevel=3, 48 | )) 49 | if not create_callback.Success: 50 | logger.info(create_callback.Error) 51 | else: 52 | logger.info(create_callback.CallbackUUID) 53 | return resp 54 | -------------------------------------------------------------------------------- /documentation-payload/bloodhound/contributing/Tasks.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "Creating a New Task" 3 | chapter = false 4 | weight = 25 5 | +++ 6 | 7 | ## Creating a New Task 8 | 9 | Creating a new task means creating a new Python file to define what parameters users should define and which API endpoint(s) for Bloodhound to utilize. 10 | 11 | New command functionality is placed in the `bloodhound/bloodhound/agent_functions` folder with an appropriately named .py file. 12 | 13 | ### Examples 14 | 15 | Here's a basic example of making a request to a static URI in bloodhound: 16 | 17 | ```python 18 | uri = '/api/v2/available-domains' 19 | 20 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, method='GET', uri=uri) 21 | ``` 22 | 23 | The taskData comes from the `create_go_tasking` function for the command as a parameter, so you can just pass that along. 24 | That `taskData` data specifically has the backing payload's build parameters and the issuing user's secrets. 25 | These two things together allow the BloodhoundAPI.query_bloodhound to lookup and properly authenticate for requests. 26 | 27 | `response_code` will vary depending on the specific bloodhound API that you're using, so I recommend checking their documentation first. 28 | 29 | `response_data` will vary depending on the specific bloodhound API that you're using, but it'll either be JSON if things were successful or a string if there was an error. 30 | 31 | ### Default Response Processing 32 | 33 | To make things easier, there is a `process_standard_response` functionality provided as part of BloodhoundAPI: 34 | 35 | ```python 36 | await BloodhoundAPI.process_standard_response(response_code=response_code, 37 | response_data=response_data, taskData=taskData, response=response) 38 | ``` 39 | 40 | This function uses the `taskData` from your `create_go_tasking` and the basic `response` you created at the beginning of your `create_go_tasking` to perform some basic checks. 41 | This will return either success or error to Mythic based on the `response_code` and display the `response_data` to the user as standard output. 42 | 43 | If you want to do something special with the response from bloodhound first, then you can either manipulate the data and then call this function, or you can perform these functions yourself. 44 | 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea/ 6 | .DS_Store 7 | rabbitmq_config.json 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | pip-wheel-metadata/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | .hypothesis/ 53 | .pytest_cache/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 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 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | BloodHound Community Edition 3 |

4 |
5 | 6 | # bloodhound 7 | 8 | This is a Mythic agent for interacting with the 3rd party service, [Bloodhound](https://github.com/SpecterOps/BloodHound). 9 | 10 | This doesn't build a payload, but instead generates a "callback" within Mythic that allows you to interact with Bloodhound CE's API. This requires you to generate an [API Token](https://support.bloodhoundenterprise.io/hc/en-us/articles/11311053342619-Working-with-the-BloodHound-API) and ID. 11 | 12 | Once you have these and Bloodhound running, set these values as BLOODHOUND_API_KEY and BLOODHOUND_API_ID as user secrets in the Mythic UI (red key on your operator settings page). 13 | 14 | ## How to install an agent in this format within Mythic 15 | 16 | When it's time for you to test out your install or for another user to install your agent, it's pretty simple. Within Mythic you can run the `mythic-cli` binary to install this in one of three ways: 17 | 18 | * `sudo ./mythic-cli install github https://github.com/user/repo` to install the main branch 19 | * `sudo ./mythic-cli install github https://github.com/user/repo branchname` to install a specific branch of that repo 20 | * `sudo ./mythic-cli install folder /path/to/local/folder/cloned/from/github` to install from an already cloned down version of an agent repo 21 | 22 | Now, you might be wondering _when_ should you or a user do this to properly add your agent to their Mythic instance. There's no wrong answer here, just depends on your preference. The three options are: 23 | 24 | * Mythic is already up and going, then you can run the install script and just direct that agent's containers to start (i.e. `sudo ./mythic-cli start agentName` and if that agent has its own special C2 containers, you'll need to start them too via `sudo ./mythic-cli start c2profileName`). 25 | * Mythic is already up and going, but you want to minimize your steps, you can just install the agent and run `sudo ./mythic-cli start`. That script will first _stop_ all of your containers, then start everything back up again. This will also bring in the new agent you just installed. 26 | * Mythic isn't running, you can install the script and just run `sudo ./mythic-cli start`. 27 | 28 | ## Documentation 29 | 30 | View the rendered documentation by clicking on **Docs -> Agent Documentation** in the upper right-hand corner of the Mythic interface. -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/whoami.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class WhoamiArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | ] 12 | 13 | async def parse_arguments(self): 14 | pass 15 | 16 | 17 | class WhoamiDomains(CommandBase): 18 | cmd = "whoami" 19 | needs_admin = False 20 | help_cmd = "whoami" 21 | description = "Get information about your current authenticated user for logging into Bloodhound" 22 | version = 1 23 | author = "@its_a_feature_" 24 | argument_class = WhoamiArguments 25 | attackmapping = [] 26 | 27 | async def create_go_tasking(self, 28 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 29 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 30 | TaskID=taskData.Task.ID, 31 | Success=False, 32 | Completed=True 33 | ) 34 | uri = '/api/v2/self' 35 | try: 36 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 37 | method='GET', 38 | uri=uri) 39 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 40 | response_data=response_data, 41 | taskData=taskData, 42 | response=response) 43 | 44 | except Exception as e: 45 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 46 | TaskID=taskData.Task.ID, 47 | Response=f"{e}".encode("UTF8"), 48 | )) 49 | response.TaskStatus = "Error: Bloodhound Access Error" 50 | return response 51 | 52 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 53 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 54 | return resp 55 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_domains.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetDomainsArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | ] 12 | 13 | async def parse_arguments(self): 14 | pass 15 | 16 | 17 | class GetDomains(CommandBase): 18 | cmd = "get_domains" 19 | needs_admin = False 20 | help_cmd = "get_domains" 21 | description = "Get the list of domains that Bloodhound is aware of via the /api/v2/available-domains API" 22 | version = 1 23 | author = "@its_a_feature_" 24 | argument_class = GetDomainsArguments 25 | attackmapping = [] 26 | 27 | async def create_go_tasking(self, 28 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 29 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 30 | TaskID=taskData.Task.ID, 31 | Success=False, 32 | Completed=True 33 | ) 34 | uri = '/api/v2/available-domains' 35 | try: 36 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 37 | method='GET', 38 | uri=uri) 39 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 40 | response_data=response_data, 41 | taskData=taskData, 42 | response=response) 43 | 44 | except Exception as e: 45 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 46 | TaskID=taskData.Task.ID, 47 | Response=f"{e}".encode("UTF8"), 48 | )) 49 | response.TaskStatus = "Error: Bloodhound Access Error" 50 | return response 51 | 52 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 53 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 54 | return resp 55 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher_list_saved.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherListSavedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | ] 12 | 13 | async def parse_arguments(self): 14 | pass 15 | 16 | async def parse_dictionary(self, dictionary_arguments): 17 | return self.load_args_from_dictionary(dictionary=dictionary_arguments) 18 | 19 | 20 | class CypherListSaved(CommandBase): 21 | cmd = "cypher_list_saved" 22 | needs_admin = False 23 | help_cmd = cmd 24 | description = "List out current user-saved queries" 25 | version = 1 26 | author = "@its_a_feature_" 27 | argument_class = CypherListSavedArguments 28 | browser_script = BrowserScript(script_name="cypher_list_saved", author="@its_a_feature_", for_new_ui=True) 29 | attackmapping = [] 30 | 31 | async def create_go_tasking(self, 32 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 33 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 34 | TaskID=taskData.Task.ID, 35 | Success=False, 36 | Completed=True, 37 | DisplayParams=f"" 38 | ) 39 | try: 40 | user_id = await BloodhoundAPI.get_whoami(taskData=taskData) 41 | uri = f"/api/v2/saved-queries?sort_by=name&user_id={user_id}" 42 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, method='GET', uri=uri) 43 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 44 | response_data=response_data, 45 | taskData=taskData, 46 | response=response) 47 | 48 | except Exception as e: 49 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 50 | TaskID=taskData.Task.ID, 51 | Response=f"{e}".encode("UTF8"), 52 | )) 53 | response.TaskStatus = "Error: Bloodhound Access Error" 54 | return response 55 | 56 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 57 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 58 | return resp 59 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_owned.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetOwnedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | 12 | ] 13 | 14 | async def parse_arguments(self): 15 | #self.load_args_from_json_string(self.command_line) 16 | pass 17 | 18 | async def parse_dictionary(self, dictionary_arguments): 19 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 20 | 21 | 22 | class GetOwned(CommandBase): 23 | cmd = "get_owned" 24 | needs_admin = False 25 | help_cmd = "get_owned" 26 | description = "Get information about owned objects" 27 | version = 1 28 | author = "@its_a_feature_" 29 | argument_class = GetOwnedArguments 30 | supported_ui_features = [] 31 | browser_script = BrowserScript(script_name="get_owned", author="@its_a_feature_", for_new_ui=True) 32 | attackmapping = [] 33 | 34 | async def create_go_tasking(self, 35 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 36 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 37 | TaskID=taskData.Task.ID, 38 | Success=False, 39 | Completed=True, 40 | DisplayParams=f"" 41 | ) 42 | 43 | try: 44 | owned_id = await BloodhoundAPI.get_owned_id(taskData) 45 | uri = f"/api/v2/asset-groups/{owned_id}/members?limit=100" 46 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 47 | method='GET', 48 | uri=uri) 49 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 50 | response_data=response_data, 51 | taskData=taskData, 52 | response=response) 53 | 54 | except Exception as e: 55 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 56 | TaskID=taskData.Task.ID, 57 | Response=f"{e}".encode("UTF8"), 58 | )) 59 | response.TaskStatus = "Error: Bloodhound Access Error" 60 | return response 61 | 62 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 63 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 64 | return resp 65 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/graph_search.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GraphSearchArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query", 13 | description="What to query Bloodhound for", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply a query") 26 | self.add_arg("query", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GraphSearch(CommandBase): 33 | cmd = "graph_search" 34 | needs_admin = False 35 | help_cmd = "graph_search -query \"my query\"" 36 | description = "Search Bloodhound Nodes via the /api/v2/graph-search API" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GraphSearchArguments 40 | browser_script = BrowserScript(script_name="graph_search", author="@its_a_feature_", for_new_ui=True) 41 | attackmapping = [] 42 | 43 | async def create_go_tasking(self, 44 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 45 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 46 | TaskID=taskData.Task.ID, 47 | Success=False, 48 | Completed=True, 49 | DisplayParams=f" for {taskData.args.get_arg('query')}" 50 | ) 51 | uri = f"/api/v2/graph-search?query={taskData.args.get_arg('query')}" 52 | try: 53 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 54 | method='GET', 55 | uri=uri) 56 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 57 | response_data=response_data, 58 | taskData=taskData, 59 | response=response) 60 | 61 | except Exception as e: 62 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 63 | TaskID=taskData.Task.ID, 64 | Response=f"{e}".encode("UTF8"), 65 | )) 66 | response.TaskStatus = "Error: Bloodhound Access Error" 67 | return response 68 | 69 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 70 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 71 | return resp 72 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_object.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetObjectArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Object to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetObject(CommandBase): 33 | cmd = "get_object" 34 | needs_admin = False 35 | help_cmd = "get_object -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 36 | description = "Get information about a specific object" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetObjectArguments 40 | supported_ui_features = ["bloodhound:get_object"] 41 | #browser_script = BrowserScript(script_name="controllables", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/base/{taskData.args.get_arg('object_id')}" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_group.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetGroupArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Group object_id to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetGroup(CommandBase): 33 | cmd = "get_group" 34 | needs_admin = False 35 | help_cmd = "get_group -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 36 | description = "Get information about a specific group" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetGroupArguments 40 | supported_ui_features = ["bloodhound:get_group"] 41 | #browser_script = BrowserScript(script_name="controllables", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/groups/{taskData.args.get_arg('object_id')}?limit=100" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/get_owned.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data['members'].length; i++){ 13 | output_table.push({ 14 | "name":{"plaintext": data['members'][i]["name"], "copyIcon": true}, 15 | "type": {"plaintext": data['members'][i]["primary_kind"]}, 16 | "object_id": {"plaintext": data['members'][i]["object_id"], "copyIcon": true}, 17 | "environment_type": {"plaintext": data['members'][i]["environment_kind"]}, 18 | "actions": {"button": { 19 | "name": "Actions", 20 | "type": "menu", 21 | "value": [ 22 | { 23 | "name": "View All Data", 24 | "type": "dictionary", 25 | "value": data['members'][i], 26 | "leftColumnTitle": "Key", 27 | "rightColumnTitle": "Value", 28 | "title": "Viewing Object Data" 29 | }, 30 | { 31 | "name": "Remove As Owned", 32 | "type": "task", 33 | "ui_feature": "bloodhound:mark_owned", 34 | "parameters": {"object_id": data['members'][i]["object_id"], "remove": true}, 35 | "startIcon": "delete" 36 | } 37 | ] 38 | }}, 39 | }) 40 | } 41 | return { 42 | "table": [ 43 | { 44 | "headers": [ 45 | {"plaintext": "name", "type": "string", "fillWidth": true}, 46 | {"plaintext": "type", "type": "string", "width": 200}, 47 | {"plaintext": "object_id", "type": "string", "width": 400}, 48 | {"plaintext": "environment_type", "type": "string", "width": 200}, 49 | {"plaintext": "actions", "type": "button", "width": 100}, 50 | ], 51 | "rows": output_table, 52 | } 53 | ] 54 | } 55 | }catch(error){ 56 | console.log(error); 57 | const combined = responses.reduce( (prev, cur) => { 58 | return prev + cur; 59 | }, ""); 60 | return {'plaintext': combined}; 61 | } 62 | }else{ 63 | return {"plaintext": "No output from command"}; 64 | } 65 | }else{ 66 | return {"plaintext": "No data to display..."}; 67 | } 68 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_group_members.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetGroupMembersArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Group object_id to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetGroupMembers(CommandBase): 33 | cmd = "get_group_members" 34 | needs_admin = False 35 | help_cmd = "get_group_members -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 36 | description = "Get information about which groups/users belong to this group" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetGroupMembersArguments 40 | supported_ui_features = ["bloodhound:get_group_members"] 41 | browser_script = BrowserScript(script_name="get_group_members", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/groups/{taskData.args.get_arg('object_id')}/members?limit=100" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_user_memberships.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetUserMembershipsArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Group object_id to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetUserMemberships(CommandBase): 33 | cmd = "get_user_memberships" 34 | needs_admin = False 35 | help_cmd = "get_user_memberships -object_id \"S-1-5-21-942593832-2479470506-1224757232-1113\"" 36 | description = "Get information about which groups this user belongs to" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetUserMembershipsArguments 40 | supported_ui_features = ["bloodhound:get_user_memberships"] 41 | browser_script = BrowserScript(script_name="get_group_members", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/users/{taskData.args.get_arg('object_id')}/memberships?limit=100" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_group_memberships.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetGroupMembershipsArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Group object_id to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetGroupMemberships(CommandBase): 33 | cmd = "get_group_memberships" 34 | needs_admin = False 35 | help_cmd = "get_group_memberships -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 36 | description = "Get information about which groups this group belongs to" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetGroupMembershipsArguments 40 | supported_ui_features = ["bloodhound:get_group_memberships"] 41 | browser_script = BrowserScript(script_name="get_group_members", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/groups/{taskData.args.get_arg('object_id')}/memberships?limit=100" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/upload_status.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | function getStatusString(entry) { 3 | if(entry["status_message"] !== ""){ 4 | return entry["status_message"]; 5 | } 6 | switch (entry["status"]){ 7 | case -1: 8 | return "Invalid Job"; 9 | case 0: 10 | return "Ready"; 11 | case 1: 12 | return "Running"; 13 | case 2: 14 | return "Complete"; 15 | case 3: 16 | return "Canceled"; 17 | case 4: 18 | return "Timed Out"; 19 | case 5: 20 | return "Failed"; 21 | case 6: 22 | return "Ingesting"; 23 | case 7: 24 | return "Analyzing"; 25 | case 8: 26 | return "Partially Complete"; 27 | default: 28 | return "Unknown Status" 29 | } 30 | } 31 | if(task.status.includes("error")){ 32 | const combined = responses.reduce( (prev, cur) => { 33 | return prev + cur; 34 | }, ""); 35 | return {'plaintext': combined}; 36 | }else if(task.completed){ 37 | if(responses.length > 0){ 38 | try{ 39 | let data = JSON.parse(responses[0]); 40 | let output_table = []; 41 | for(let i = 0; i < data['data'].length; i++){ 42 | let currentData = data['data'][i]; 43 | output_table.push({ 44 | "start_time":{"plaintext": currentData["start_time"]}, 45 | "end_time": {"plaintext": currentData["end_time"]}, 46 | "id": {"plaintext": currentData["id"]}, 47 | "status": {"plaintext": getStatusString(currentData)}, 48 | "fetch": {"button": { 49 | "name": "Fetch", 50 | "type": "task", 51 | "ui_feature": "bloodhound:upload_status", 52 | "parameters": JSON.stringify({"id":currentData['id']}) 53 | } 54 | 55 | }, 56 | }) 57 | } 58 | return { 59 | "table": [ 60 | { 61 | "headers": [ 62 | {"plaintext": "start_time", "type": "date", "fillWidth": true}, 63 | {"plaintext": "end_time", "type": "date", "fillWidth": true}, 64 | {"plaintext": "id", "type": "number", "fillWidth": true}, 65 | {"plaintext": "status", "type": "string", "fillWidth": true}, 66 | {"plaintext": "fetch", "type": "button", "width": 200}, 67 | ], 68 | "rows": output_table, 69 | "title": "Upload Status" 70 | } 71 | ] 72 | } 73 | }catch(error){ 74 | console.log(error); 75 | const combined = responses.reduce( (prev, cur) => { 76 | return prev + cur; 77 | }, ""); 78 | return {'plaintext': combined}; 79 | } 80 | }else{ 81 | return {"plaintext": "No output from command"}; 82 | } 83 | }else{ 84 | return {"plaintext": "No data to display..."}; 85 | } 86 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query", 13 | description="What to query Bloodhound for", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply a query") 26 | self.add_arg("query", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class Cypher(CommandBase): 33 | cmd = "cypher" 34 | needs_admin = False 35 | help_cmd = "cypher -query \"MATCH (n:User)WHERE n.hasspn=true RETURN n\"" 36 | description = "Run a cypher query against Bloodhound via the /api/v2/graphs/cypher API" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = CypherArguments 40 | supported_ui_features = ["bloodhound:cypher"] 41 | browser_script = BrowserScript(script_name="cypher", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('query')}" 51 | ) 52 | uri = f"/api/v2/graphs/cypher" 53 | body = json.dumps({ 54 | "include_properties": True, 55 | "query": taskData.args.get_arg("query") 56 | }).encode() 57 | try: 58 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 59 | method='POST', 60 | body=body, 61 | uri=uri) 62 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 63 | response_data=response_data, 64 | taskData=taskData, 65 | response=response) 66 | 67 | except Exception as e: 68 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 69 | TaskID=taskData.Task.ID, 70 | Response=f"{e}".encode("UTF8"), 71 | )) 72 | response.TaskStatus = "Error: Bloodhound Access Error" 73 | return response 74 | 75 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 76 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 77 | return resp 78 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_domains_foreign_users.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetDomainsForeignUsersArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which domain object_id to query to see if there are foreign users in groups in that domain", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | 21 | ] 22 | 23 | async def parse_arguments(self): 24 | if len(self.command_line) == 0: 25 | raise ValueError("Must supply an object_id") 26 | self.add_arg("object_id", self.command_line) 27 | 28 | async def parse_dictionary(self, dictionary_arguments): 29 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 30 | 31 | 32 | class GetDomainsForeignUsers(CommandBase): 33 | cmd = "get_domains_foreign_users" 34 | needs_admin = False 35 | help_cmd = "get_domains_foreign_users -object_id \"S-1-5-21-909015691-3030120388-2582151266\"" 36 | description = "Get information about foreign users in groups within the specified domain" 37 | version = 1 38 | author = "@its_a_feature_" 39 | argument_class = GetDomainsForeignUsersArguments 40 | supported_ui_features = ["bloodhound:get_domains_foreign_users"] 41 | #browser_script = BrowserScript(script_name="controllables", author="@its_a_feature_", for_new_ui=True) 42 | attackmapping = [] 43 | 44 | async def create_go_tasking(self, 45 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 46 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 47 | TaskID=taskData.Task.ID, 48 | Success=False, 49 | Completed=True, 50 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 51 | ) 52 | uri = f"/api/v2/domains/{taskData.args.get_arg('object_id')}/foreign-users?limit=100" 53 | try: 54 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 55 | method='GET', 56 | uri=uri) 57 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 58 | response_data=response_data, 59 | taskData=taskData, 60 | response=response) 61 | 62 | except Exception as e: 63 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 64 | TaskID=taskData.Task.ID, 65 | Response=f"{e}".encode("UTF8"), 66 | )) 67 | response.TaskStatus = "Error: Bloodhound Access Error" 68 | return response 69 | 70 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 71 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 72 | return resp 73 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/controllables.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data.length; i++){ 13 | output_table.push({ 14 | "name":{"plaintext": data[i]["name"], "copyIcon": true}, 15 | "type": {"plaintext": data[i]["label"]}, 16 | "object_id": {"plaintext": data[i]["objectID"], "copyIcon": true}, 17 | "actions": {"button": { 18 | "name": "Actions", 19 | "type": "menu", 20 | "value": [ 21 | { 22 | "name": "View All Data", 23 | "type": "dictionary", 24 | "value": data[i], 25 | "leftColumnTitle": "Key", 26 | "rightColumnTitle": "Value", 27 | "title": "Viewing Object Data" 28 | }, 29 | { 30 | "name": "Get Object Info", 31 | "type": "task", 32 | "parameters": JSON.stringify({"object_id": data[i]["objectID"]}), 33 | "ui_feature": "bloodhound:get_object" 34 | }, 35 | { 36 | "name": "Mark As Owned", 37 | "type": "task", 38 | "ui_feature": "bloodhound:mark_owned", 39 | "parameters": {"object_id": data[i]["objectID"]}, 40 | "startIcon": "kill" 41 | } 42 | ] 43 | }}, 44 | }) 45 | } 46 | return { 47 | "table": [ 48 | { 49 | "headers": [ 50 | {"plaintext": "name", "type": "string", "fillWidth": true}, 51 | {"plaintext": "type", "type": "string", "width": 200}, 52 | {"plaintext": "object_id", "type": "string", "width": 400}, 53 | {"plaintext": "actions", "type": "button", "width": 100}, 54 | ], 55 | "rows": output_table, 56 | "title": "Search Results" 57 | } 58 | ] 59 | } 60 | }catch(error){ 61 | console.log(error); 62 | const combined = responses.reduce( (prev, cur) => { 63 | return prev + cur; 64 | }, ""); 65 | return {'plaintext': combined}; 66 | } 67 | }else{ 68 | return {"plaintext": "No output from command"}; 69 | } 70 | }else{ 71 | return {"plaintext": "No data to display..."}; 72 | } 73 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/BloodhoundRequests/BloodhoundAPIClasses.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | import base64 4 | import requests 5 | import datetime 6 | 7 | from typing import Optional 8 | 9 | DATA_START = "1970-01-01T00:00:00.000Z" 10 | DATA_END = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' # Now 11 | 12 | 13 | class Credentials(object): 14 | def __init__(self, token_id: str, token_key: str) -> None: 15 | self.token_id = token_id 16 | self.token_key = token_key 17 | 18 | 19 | class Client(object): 20 | def __init__(self, url: str, credentials: Credentials) -> None: 21 | self._url = url 22 | self._credentials = credentials 23 | 24 | def _format_url(self, uri: str) -> str: 25 | formatted_uri = uri 26 | if uri.startswith("/"): 27 | formatted_uri = formatted_uri[1:] 28 | 29 | return f"{self._url}/{formatted_uri}" 30 | 31 | def Request(self, method: str, uri: str, body: Optional[bytes] = None) -> requests.Response: 32 | # Digester is initialized with HMAC-SHA-256 using the token key as the HMAC digest key. 33 | digester = hmac.new(self._credentials.token_key.encode(), None, hashlib.sha256) 34 | 35 | # OperationKey is the first HMAC digest link in the signature chain. This prevents replay attacks that seek to 36 | # modify the request method or URI. It is composed of concatenating the request method and the request URI with 37 | # no delimiter and computing the HMAC digest using the token key as the digest secret. 38 | # 39 | # Example: GET /api/v1/test/resource HTTP/1.1 40 | # Signature Component: GET/api/v1/test/resource 41 | digester.update(f"{method}{uri}".encode()) 42 | 43 | # Update the digester for further chaining 44 | digester = hmac.new(digester.digest(), None, hashlib.sha256) 45 | 46 | # DateKey is the next HMAC digest link in the signature chain. This encodes the RFC3339 formatted datetime 47 | # value as part of the signature to the hour to prevent replay attacks that are older than max two hours. This 48 | # value is added to the signature chain by cutting off all values from the RFC3339 formatted datetime from the 49 | # hours value forward: 50 | # 51 | # Example: 2020-12-01T23:59:60Z 52 | # Signature Component: 2020-12-01T23 53 | datetime_formatted = datetime.datetime.now().astimezone().isoformat("T") 54 | digester.update(datetime_formatted[:13].encode()) 55 | 56 | # Update the digester for further chaining 57 | digester = hmac.new(digester.digest(), None, hashlib.sha256) 58 | 59 | # Body signing is the last HMAC digest link in the signature chain. This encodes the request body as part of 60 | # the signature to prevent replay attacks that seek to modify the payload of a signed request. In the case 61 | # where there is no body content the HMAC digest is computed anyway, simply with no values written to the 62 | # digester. 63 | if body is not None: 64 | digester.update(body) 65 | 66 | # Perform the request with the signed and expected headers 67 | return requests.request( 68 | method=method, 69 | url=self._format_url(uri), 70 | headers={ 71 | "User-Agent": "bhe-python-sdk 0001", 72 | "Authorization": f"bhesignature {self._credentials.token_id}", 73 | "RequestDate": datetime_formatted, 74 | "Signature": base64.b64encode(digester.digest()), 75 | "Content-Type": "application/json", 76 | }, 77 | data=body, 78 | ) 79 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher_delete_saved.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherDeleteSavedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query_id", 13 | description="ID for the saved query to delete", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | ] 21 | 22 | async def parse_arguments(self): 23 | self.load_args_from_json_string(command_line=self.command_line) 24 | 25 | async def parse_dictionary(self, dictionary_arguments): 26 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 27 | 28 | 29 | class CypherDeleteSaved(CommandBase): 30 | cmd = "cypher_delete_saved" 31 | needs_admin = False 32 | help_cmd = "cypher_delete_saved -query_id 4" 33 | description = "Delete a new saved custom cypher" 34 | version = 1 35 | author = "@its_a_feature_" 36 | argument_class = CypherDeleteSavedArguments 37 | supported_ui_features = ["bloodhound:cypher_delete_saved"] 38 | #browser_script = BrowserScript(script_name="cypher_list_saved", author="@its_a_feature_", for_new_ui=True) 39 | attackmapping = [] 40 | 41 | async def create_go_tasking(self, 42 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 43 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 44 | TaskID=taskData.Task.ID, 45 | Success=False, 46 | Completed=True, 47 | DisplayParams=f" for {taskData.args.get_arg('query_id')}" 48 | ) 49 | uri = f"/api/v2/saved-queries/{taskData.args.get_arg('query_id')}" 50 | try: 51 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 52 | method='DELETE', 53 | uri=uri) 54 | if response_code == 204: 55 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 56 | TaskID=taskData.Task.ID, 57 | Response=f"Successfully deleted query".encode("UTF8"), 58 | )) 59 | response.Success = True 60 | return response 61 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 62 | response_data=response_data, 63 | taskData=taskData, 64 | response=response) 65 | 66 | except Exception as e: 67 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 68 | TaskID=taskData.Task.ID, 69 | Response=f"{e}".encode("UTF8"), 70 | )) 71 | response.TaskStatus = "Error: Bloodhound Access Error" 72 | return response 73 | 74 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 75 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 76 | return resp 77 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/controllables.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class ControllablesArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which Object to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | CommandParameter( 21 | name="skip", 22 | type=ParameterType.Number, 23 | default_value=0, 24 | parameter_group_info=[ParameterGroupInfo( 25 | required=False, 26 | ui_position=3 27 | )] 28 | ), 29 | 30 | ] 31 | 32 | async def parse_arguments(self): 33 | if len(self.command_line) == 0: 34 | raise ValueError("Must supply an object_id") 35 | self.add_arg("object_id", self.command_line) 36 | 37 | async def parse_dictionary(self, dictionary_arguments): 38 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 39 | 40 | 41 | class Controllables(CommandBase): 42 | cmd = "controllables" 43 | needs_admin = False 44 | help_cmd = "controllables -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 45 | description = "Search for Nodes that the specified object has control over" 46 | version = 1 47 | author = "@its_a_feature_" 48 | argument_class = ControllablesArguments 49 | supported_ui_features = ["bloodhound:controllables"] 50 | browser_script = BrowserScript(script_name="controllables", author="@its_a_feature_", for_new_ui=True) 51 | attackmapping = [] 52 | 53 | async def create_go_tasking(self, 54 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 55 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 56 | TaskID=taskData.Task.ID, 57 | Success=False, 58 | Completed=True, 59 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 60 | ) 61 | uri = f"/api/v2/base/{taskData.args.get_arg('object_id')}/controllables?skip={taskData.args.get_arg('skip')}&limit=100" 62 | try: 63 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 64 | method='GET', 65 | uri=uri) 66 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 67 | response_data=response_data, 68 | taskData=taskData, 69 | response=response) 70 | 71 | except Exception as e: 72 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 73 | TaskID=taskData.Task.ID, 74 | Response=f"{e}".encode("UTF8"), 75 | )) 76 | response.TaskStatus = "Error: Bloodhound Access Error" 77 | return response 78 | 79 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 80 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 81 | return resp 82 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher_create_saved.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherCreateSavedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="name", 13 | description="Name for new saved custom query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | CommandParameter( 21 | name="query", 22 | description="The associated cypher query to save", 23 | type=ParameterType.String, 24 | parameter_group_info=[ParameterGroupInfo( 25 | required=True, 26 | ui_position=2 27 | )] 28 | ), 29 | 30 | ] 31 | 32 | async def parse_arguments(self): 33 | self.load_args_from_json_string(command_line=self.command_line) 34 | 35 | async def parse_dictionary(self, dictionary_arguments): 36 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 37 | 38 | 39 | class CypherCreateSaved(CommandBase): 40 | cmd = "cypher_create_saved" 41 | needs_admin = False 42 | help_cmd = "cypher_create_saved -name \"My custom query\" -query \"MATCH (n:User)WHERE n.hasspn=true RETURN n\"" 43 | description = "Create a new saved custom cypher" 44 | version = 1 45 | author = "@its_a_feature_" 46 | argument_class = CypherCreateSavedArguments 47 | supported_ui_features = ["bloodhound:cypher_create_saved"] 48 | #browser_script = BrowserScript(script_name="cypher", author="@its_a_feature_", for_new_ui=True) 49 | attackmapping = [] 50 | 51 | async def create_go_tasking(self, 52 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 53 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 54 | TaskID=taskData.Task.ID, 55 | Success=False, 56 | Completed=True, 57 | DisplayParams=f" for {taskData.args.get_arg('name')}" 58 | ) 59 | uri = f"/api/v2/saved-queries" 60 | body = json.dumps({ 61 | "name": taskData.args.get_arg("name"), 62 | "query": taskData.args.get_arg("query") 63 | }).encode() 64 | try: 65 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 66 | method='POST', 67 | body=body, 68 | uri=uri) 69 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 70 | response_data=response_data, 71 | taskData=taskData, 72 | response=response) 73 | 74 | except Exception as e: 75 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 76 | TaskID=taskData.Task.ID, 77 | Response=f"{e}".encode("UTF8"), 78 | )) 79 | response.TaskStatus = "Error: Bloodhound Access Error" 80 | return response 81 | 82 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 83 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 84 | return resp 85 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/cypher_list_saved.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data.length; i++){ 13 | output_table.push({ 14 | "name":{"plaintext": data[i]["name"], "copyIcon": true}, 15 | "cypher": {"plaintext": data[i]["query"]}, 16 | "actions": {"button": { 17 | "name": "Actions", 18 | "type": "menu", 19 | "value": [ 20 | { 21 | "name": "View All Data", 22 | "type": "dictionary", 23 | "value": data[i], 24 | "leftColumnTitle": "Key", 25 | "rightColumnTitle": "Value", 26 | "title": "Viewing Object Data" 27 | }, 28 | { 29 | "name": "Run Query", 30 | "type": "task", 31 | "parameters": JSON.stringify({"query": data[i]["query"]}), 32 | "ui_feature": "bloodhound:cypher" 33 | }, 34 | { 35 | "name": "Delete Query", 36 | "type": "task", 37 | "ui_feature": "bloodhound:cypher_delete_saved", 38 | "parameters": {"query_id": data[i]["id"]}, 39 | "startIcon": "kill", 40 | "getConfirmation": true, 41 | } 42 | ] 43 | }}, 44 | }) 45 | } 46 | output_table.push({ 47 | "name": "", 48 | "query": "", 49 | "actions": {"button": { 50 | "name": "Save New Query", 51 | "type": "task", 52 | "ui_feature": "bloodhound:cypher_create_saved", 53 | "openDialog": true, 54 | }} 55 | }) 56 | return { 57 | "table": [ 58 | { 59 | "headers": [ 60 | {"plaintext": "name", "type": "string", "fillWidth": true}, 61 | {"plaintext": "cypher", "type": "string", "fillWidth": true}, 62 | {"plaintext": "actions", "type": "button", "width": 70}, 63 | ], 64 | "rows": output_table, 65 | "title": "Saved Cypher Queries" 66 | } 67 | ] 68 | } 69 | }catch(error){ 70 | console.log(error); 71 | const combined = responses.reduce( (prev, cur) => { 72 | return prev + cur; 73 | }, ""); 74 | return {'plaintext': combined}; 75 | } 76 | }else{ 77 | return {"plaintext": "No output from command"}; 78 | } 79 | }else{ 80 | return {"plaintext": "No data to display..."}; 81 | } 82 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/search.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data.length; i++){ 13 | output_table.push({ 14 | "name":{"plaintext": data[i]["name"], "copyIcon": true}, 15 | "type": {"plaintext": data[i]["type"]}, 16 | "object_id": {"plaintext": data[i]["objectid"], "copyIcon": true}, 17 | "actions": {"button": { 18 | "name": "Actions", 19 | "type": "menu", 20 | "value": [ 21 | { 22 | "name": "View All Data", 23 | "type": "dictionary", 24 | "value": data[i], 25 | "leftColumnTitle": "Key", 26 | "rightColumnTitle": "Value", 27 | "title": "Viewing Object Data" 28 | }, 29 | { 30 | "name": "Controllables", 31 | "type": "task", 32 | "ui_feature": "bloodhound:controllables", 33 | "parameters": JSON.stringify({"object_id": data[i]["objectid"]}) 34 | }, 35 | { 36 | "name": "Get Object", 37 | "type": "task", 38 | "ui_feature": "bloodhound:get_object", 39 | "parameters": JSON.stringify({"object_id": data[i]["objectid"]}) 40 | }, 41 | { 42 | "name": "Mark As Owned", 43 | "type": "task", 44 | "ui_feature": "bloodhound:mark_owned", 45 | "parameters": {"object_id": data[i]["objectid"]}, 46 | "startIcon": "kill" 47 | } 48 | ] 49 | }}, 50 | }) 51 | } 52 | return { 53 | "table": [ 54 | { 55 | "headers": [ 56 | {"plaintext": "name", "type": "string", "fillWidth": true}, 57 | {"plaintext": "type", "type": "string", "width": 200}, 58 | {"plaintext": "object_id", "type": "string", "width": 400}, 59 | {"plaintext": "actions", "type": "button", "width": 100}, 60 | ], 61 | "rows": output_table, 62 | "title": "Search Results" 63 | } 64 | ] 65 | } 66 | }catch(error){ 67 | console.log(error); 68 | const combined = responses.reduce( (prev, cur) => { 69 | return prev + cur; 70 | }, ""); 71 | return {'plaintext': combined}; 72 | } 73 | }else{ 74 | return {"plaintext": "No output from command"}; 75 | } 76 | }else{ 77 | return {"plaintext": "No data to display..."}; 78 | } 79 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/get_group_members.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(let i = 0; i < data.length; i++){ 13 | output_table.push({ 14 | "object_id":{"plaintext": data[i]["objectID"], "copyIcon": true}, 15 | "name": {"plaintext": data[i]["name"]}, 16 | "label": {"plaintext": data[i]["label"], "copyIcon": true}, 17 | "actions": {"button": { 18 | "name": "Actions", 19 | "type": "menu", 20 | "value": [ 21 | { 22 | "name": "View All Data", 23 | "type": "dictionary", 24 | "value": data[i], 25 | "leftColumnTitle": "Key", 26 | "rightColumnTitle": "Value", 27 | "title": "Viewing Object Data" 28 | }, 29 | { 30 | "name": "Mark As Owned", 31 | "type": "task", 32 | "ui_feature": "bloodhound:mark_owned", 33 | "parameters": {"object_id": data[i]["objectID"], "remove": false}, 34 | "startIcon": "kill" 35 | }, 36 | { 37 | "name": "Get Object Information", 38 | "type": "task", 39 | "ui_feature": "bloodhound:get_object", 40 | "parameters": {"object_id": data[i]["objectID"]} 41 | }, 42 | { 43 | "name": `Get ${data[i]["label"].toLowerCase()}'s Memberships`, 44 | "type": "task", 45 | "ui_feature": `bloodhound:get_${data[i]["label"].toLowerCase()}_memberships`, 46 | "parameters": {"object_id": data[i]["objectID"]} 47 | } 48 | ] 49 | }}, 50 | }) 51 | } 52 | return { 53 | "table": [ 54 | { 55 | "headers": [ 56 | {"plaintext": "object_id", "type": "string", "fillWidth": true}, 57 | {"plaintext": "name", "type": "string", "fillWidth": true}, 58 | {"plaintext": "label", "type": "string", "width": 100}, 59 | {"plaintext": "actions", "type": "button", "width": 100}, 60 | ], 61 | "rows": output_table, 62 | } 63 | ] 64 | } 65 | }catch(error){ 66 | console.log(error); 67 | const combined = responses.reduce( (prev, cur) => { 68 | return prev + cur; 69 | }, ""); 70 | return {'plaintext': combined}; 71 | } 72 | }else{ 73 | return {"plaintext": "No output from command"}; 74 | } 75 | }else{ 76 | return {"plaintext": "No data to display..."}; 77 | } 78 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/graph_search.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | if(task.status.includes("error")){ 3 | const combined = responses.reduce( (prev, cur) => { 4 | return prev + cur; 5 | }, ""); 6 | return {'plaintext': combined}; 7 | }else if(task.completed){ 8 | if(responses.length > 0){ 9 | try{ 10 | let data = JSON.parse(responses[0]); 11 | let output_table = []; 12 | for(const[key, value] of Object.entries(data)){ 13 | output_table.push({ 14 | "name":{"plaintext": value.data["name"], "copyIcon": true}, 15 | "type": {"plaintext": value.data["nodetype"]}, 16 | "object_id": {"plaintext": value.data["object_id"], "copyIcon": true}, 17 | "actions": {"button": { 18 | "name": "Actions", 19 | "type": "menu", 20 | "value": [ 21 | { 22 | "name": "View All Data", 23 | "type": "dictionary", 24 | "value": value.data, 25 | "leftColumnTitle": "Key", 26 | "rightColumnTitle": "Value", 27 | "title": "Viewing Object Data" 28 | }, 29 | { 30 | "name": "Controllables", 31 | "type": "task", 32 | "ui_feature": "bloodhound:controllables", 33 | "parameters": JSON.stringify({"object_id": value.data["objectid"]}) 34 | }, 35 | { 36 | "name": "Get Object", 37 | "type": "task", 38 | "ui_feature": "bloodhound:get_object", 39 | "parameters": JSON.stringify({"object_id": value.data["objectid"]}) 40 | }, 41 | { 42 | "name": "Mark As Owned", 43 | "type": "task", 44 | "ui_feature": "bloodhound:mark_owned", 45 | "parameters": {"object_id": value.data["objectid"]}, 46 | "startIcon": "kill" 47 | } 48 | ] 49 | }}, 50 | }) 51 | } 52 | return { 53 | "table": [ 54 | { 55 | "headers": [ 56 | {"plaintext": "name", "type": "string", "fillWidth": true}, 57 | {"plaintext": "type", "type": "string", "width": 200}, 58 | {"plaintext": "object_id", "type": "string", "width": 400}, 59 | {"plaintext": "actions", "type": "button", "width": 100}, 60 | ], 61 | "rows": output_table, 62 | "title": "Search Results" 63 | } 64 | ] 65 | } 66 | }catch(error){ 67 | console.log(error); 68 | const combined = responses.reduce( (prev, cur) => { 69 | return prev + cur; 70 | }, ""); 71 | return {'plaintext': combined}; 72 | } 73 | }else{ 74 | return {"plaintext": "No output from command"}; 75 | } 76 | }else{ 77 | return {"plaintext": "No data to display..."}; 78 | } 79 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/upload_status.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class UploadStatusArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="id", display_name="ID returned from `upload` command", type=ParameterType.Number, 13 | description="Bloodhound ID from uploading the file", 14 | default_value=0, 15 | parameter_group_info=[ 16 | ParameterGroupInfo( 17 | required=False, 18 | group_name="Default", 19 | ui_position=0 20 | ) 21 | ] 22 | ), 23 | ] 24 | 25 | async def parse_arguments(self): 26 | if len(self.command_line) == 0: 27 | raise ValueError("Must supply arguments") 28 | raise ValueError("Must supply named arguments or use the modal") 29 | 30 | async def parse_dictionary(self, dictionary_arguments): 31 | self.load_args_from_dictionary(dictionary_arguments) 32 | 33 | 34 | class UploadStatus(CommandBase): 35 | cmd = "upload_status" 36 | needs_admin = False 37 | help_cmd = "upload_status -id 6" 38 | description = "Check the upload/ingesting status for Bloodhound files" 39 | version = 1 40 | author = "@its_a_feature_" 41 | argument_class = UploadStatusArguments 42 | supported_ui_features = ["bloodhound:upload_status"] 43 | browser_script = BrowserScript(script_name="upload_status", author="@its_a_feature_", for_new_ui=True) 44 | attackmapping = [] 45 | 46 | async def create_go_tasking(self, 47 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 48 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 49 | TaskID=taskData.Task.ID, 50 | Success=False, 51 | Completed=True, 52 | ) 53 | uri = f"/api/v2/file-upload" 54 | if taskData.args.get_arg("id") > 0: 55 | uri += f"?limit=1&id=eq%3A{taskData.args.get_arg('id')}" 56 | response.DisplayParams = f"for job {taskData.args.get_arg('id')}" 57 | else: 58 | uri += f"?limit=10&sort_by=-id" 59 | response.DisplayParams = f" for the 10 most recent jobs" 60 | try: 61 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 62 | method='GET', 63 | uri=uri) 64 | if response_code != 200: 65 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 66 | response_data=response_data, 67 | taskData=taskData, 68 | response=response) 69 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 70 | TaskID=taskData.Task.ID, 71 | Response=f"{json.dumps(response_data)}".encode("UTF8"), 72 | )) 73 | except Exception as e: 74 | logger.exception(e) 75 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 76 | TaskID=taskData.Task.ID, 77 | Response=f"{e}".encode("UTF8"), 78 | )) 79 | response.TaskStatus = "Error: Bloodhound Access Error" 80 | return response 81 | 82 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 83 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 84 | return resp 85 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/search.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class SearchArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query", 13 | description="What to query Bloodhound for", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1 18 | )] 19 | ), 20 | CommandParameter( 21 | name="type", 22 | description="Filter your search results by Node type", 23 | type=ParameterType.String, 24 | default_value="", 25 | parameter_group_info=[ParameterGroupInfo( 26 | required=False, 27 | ui_position=2 28 | )] 29 | ), 30 | CommandParameter( 31 | name="skip", 32 | type=ParameterType.Number, 33 | default_value=0, 34 | parameter_group_info=[ParameterGroupInfo( 35 | required=False, 36 | ui_position=3 37 | )] 38 | ), 39 | 40 | ] 41 | 42 | async def parse_arguments(self): 43 | if len(self.command_line) == 0: 44 | raise ValueError("Must supply a query") 45 | self.add_arg("query", self.command_line) 46 | 47 | async def parse_dictionary(self, dictionary_arguments): 48 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 49 | 50 | 51 | class Search(CommandBase): 52 | cmd = "search" 53 | needs_admin = False 54 | help_cmd = "search -query \"my query\" -type \"Group\"" 55 | description = "Search Bloodhound Nodes via the /api/v2/search API" 56 | version = 1 57 | author = "@its_a_feature_" 58 | argument_class = SearchArguments 59 | browser_script = BrowserScript(script_name="search", author="@its_a_feature_", for_new_ui=True) 60 | attackmapping = [] 61 | 62 | async def create_go_tasking(self, 63 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 64 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 65 | TaskID=taskData.Task.ID, 66 | Success=False, 67 | Completed=True, 68 | DisplayParams=f" for {taskData.args.get_arg('query')}" 69 | ) 70 | uri = f"/api/v2/search?limit=100&q={taskData.args.get_arg('query')}&skip={taskData.args.get_arg('skip')}" 71 | if taskData.args.get_arg("type") != "": 72 | uri += f"&type={taskData.args.get_arg('type')}" 73 | try: 74 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 75 | method='GET', 76 | uri=uri) 77 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 78 | response_data=response_data, 79 | taskData=taskData, 80 | response=response) 81 | 82 | except Exception as e: 83 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 84 | TaskID=taskData.Task.ID, 85 | Response=f"{e}".encode("UTF8"), 86 | )) 87 | response.TaskStatus = "Error: Bloodhound Access Error" 88 | return response 89 | 90 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 91 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 92 | return resp 93 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/mark_owned.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class MarkOwnedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | cli_name="object_id", 14 | description="Which object_id to mark as owned", 15 | type=ParameterType.String, 16 | parameter_group_info=[ParameterGroupInfo( 17 | required=True, 18 | ui_position=1, 19 | group_name="mark_as_owned" 20 | )] 21 | ), 22 | CommandParameter( 23 | name="remove", 24 | cli_name="remove", 25 | description="Unmark and object as owned", 26 | type=ParameterType.Boolean, 27 | default_value=False, 28 | parameter_group_info=[ParameterGroupInfo( 29 | required=False, 30 | ui_position=1, 31 | group_name="mark_as_owned" 32 | )] 33 | ), 34 | ] 35 | 36 | async def parse_arguments(self): 37 | self.load_args_from_json_string(self.command_line) 38 | 39 | async def parse_dictionary(self, dictionary_arguments): 40 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 41 | 42 | 43 | class MarkOwned(CommandBase): 44 | cmd = "mark_owned" 45 | needs_admin = False 46 | help_cmd = "mark_owned -object_id [object id]" 47 | description = "Get information about owned objects" 48 | version = 1 49 | author = "@its_a_feature_" 50 | argument_class = MarkOwnedArguments 51 | supported_ui_features = ["bloodhound:mark_owned"] 52 | # browser_script = BrowserScript(script_name="get_owned", author="@its_a_feature_", for_new_ui=True) 53 | attackmapping = [] 54 | 55 | async def create_go_tasking(self, 56 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 57 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 58 | TaskID=taskData.Task.ID, 59 | Success=False, 60 | Completed=True, 61 | ) 62 | if taskData.args.get_arg("remove"): 63 | response.DisplayParams = f" remove {taskData.args.get_arg('object_id')}" 64 | else: 65 | response.DisplayParams = f" {taskData.args.get_arg('object_id')}" 66 | try: 67 | owned_id = await BloodhoundAPI.get_owned_id(taskData) 68 | uri = f"/api/v2/asset-groups/{owned_id}/selectors" 69 | body = json.dumps([{"action": "add" if not taskData.args.get_arg("remove") else "remove", 70 | "selector_name": taskData.args.get_arg("object_id"), 71 | "sid": taskData.args.get_arg("object_id")}]).encode() 72 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 73 | method='PUT', 74 | uri=uri, 75 | body=body) 76 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 77 | response_data=response_data, 78 | taskData=taskData, 79 | response=response) 80 | 81 | except Exception as e: 82 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 83 | TaskID=taskData.Task.ID, 84 | Response=f"{e}".encode("UTF8"), 85 | )) 86 | response.TaskStatus = "Error: Bloodhound Access Error" 87 | return response 88 | 89 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 90 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 91 | return resp 92 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/shortest_path.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class ShortestPathArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="start_node", 13 | cli_name="start", 14 | description="The object id to start from ", 15 | type=ParameterType.String, 16 | parameter_group_info=[ParameterGroupInfo( 17 | required=True, 18 | ui_position=1 19 | )] 20 | ), 21 | CommandParameter( 22 | name="end_node", 23 | cli_name="end", 24 | description="The object id to end", 25 | type=ParameterType.String, 26 | default_value="", 27 | parameter_group_info=[ParameterGroupInfo( 28 | required=True, 29 | ui_position=2 30 | )] 31 | ), 32 | CommandParameter( 33 | name="relationships", 34 | cli_name="relationships", 35 | description="Only allow certain kinds of edges", 36 | type=ParameterType.ChooseMultiple, 37 | choices=["Contains", "GetChangesAll", "MemberOf"], 38 | default_value=["Contains", "GetChangesAll", "MemberOf"], 39 | parameter_group_info=[ParameterGroupInfo( 40 | required=False, 41 | ui_position=3 42 | )] 43 | ), 44 | ] 45 | 46 | async def parse_arguments(self): 47 | self.load_args_from_json_string(self.command_line) 48 | 49 | async def parse_dictionary(self, dictionary_arguments): 50 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 51 | 52 | 53 | class ShortestPath(CommandBase): 54 | cmd = "shortest_path" 55 | needs_admin = False 56 | help_cmd = "shortest_path -start \"S-1-5-21-909015691-3030120388-2582151266-512\" -end \"S-1-5-21-909015691-3030120388-2582151266-1000\"" 57 | description = "Query Bloodhound CE for the shortest path between two nodes" 58 | version = 1 59 | author = "@its_a_feature_" 60 | argument_class = ShortestPathArguments 61 | supported_ui_features = ["bloodhound:shortest_path"] 62 | browser_script = BrowserScript(script_name="cypher", author="@its_a_feature_", for_new_ui=True) 63 | attackmapping = [] 64 | 65 | async def create_go_tasking(self, 66 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 67 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 68 | TaskID=taskData.Task.ID, 69 | Success=False, 70 | Completed=True, 71 | DisplayParams=f" for {taskData.args.get_arg('start_node')} to {taskData.args.get_arg('end_node')}" 72 | ) 73 | uri = f"/api/v2/graphs/shortest-path?start_node={taskData.args.get_arg('start_node')}&end_node={taskData.args.get_arg('end_node')}&relationship_kinds=in%3A{'%2C'.join(taskData.args.get_arg('relationships'))}" 74 | try: 75 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 76 | method='GET', 77 | uri=uri) 78 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 79 | response_data=response_data, 80 | taskData=taskData, 81 | response=response) 82 | 83 | except Exception as e: 84 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 85 | TaskID=taskData.Task.ID, 86 | Response=f"{e}".encode("UTF8"), 87 | )) 88 | response.TaskStatus = "Error: Bloodhound Access Error" 89 | return response 90 | 91 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 92 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 93 | return resp 94 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/BloodhoundRequests/BloodhoundAPI.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from bloodhound.BloodhoundRequests.BloodhoundAPIClasses import * 3 | from mythic_container.MythicRPC import * 4 | 5 | cachedAssetGroupOwnedID = None 6 | BLOODHOUND_API_KEY = "BLOODHOUND_API_KEY" 7 | BLOODHOUND_API_ID = "BLOODHOUND_API_ID" 8 | 9 | 10 | def checkValidValues(token_id, token_key, url) -> bool: 11 | if token_id == "" or token_id is None: 12 | return False 13 | if token_key == "" or token_key is None: 14 | return False 15 | if url == "" or url is None: 16 | return False 17 | return True 18 | 19 | 20 | async def query_bloodhound(taskData: PTTaskMessageAllData, uri: str, method: str = 'GET', body: bytes = None, ) -> \ 21 | (int, dict): 22 | token_id = None 23 | token_key = None 24 | url = None 25 | for buildParam in taskData.BuildParameters: 26 | if buildParam.Name == "URL": 27 | url = buildParam.Value 28 | if BLOODHOUND_API_KEY in taskData.Secrets: 29 | token_key = taskData.Secrets[BLOODHOUND_API_KEY] 30 | if BLOODHOUND_API_ID in taskData.Secrets: 31 | token_id = taskData.Secrets[BLOODHOUND_API_ID] 32 | if not checkValidValues(token_id, token_key, url): 33 | if token_id == "" or token_id is None: 34 | return 500, "Missing BLOODHOUND_API_ID in user's secrets" 35 | if token_key == "" or token_key is None: 36 | return 500, "Missing BLOODHOUND_API_KEY in user's secrets" 37 | if url == "" or url is None: 38 | return 500, "Missing URL from build parameters" 39 | 40 | try: 41 | credentials = Credentials(token_id=token_id, token_key=token_key) 42 | client = Client(url=url, credentials=credentials) 43 | response = client.Request(method=method, uri=uri, body=body) 44 | logger.info(f"Bloodhound Query: {uri}") 45 | #logger.info(response.status_code) 46 | #logger.info(response.text) 47 | if 200 <= response.status_code < 300: 48 | try: 49 | payload = response.json() 50 | return response.status_code, payload 51 | except Exception as mid_exception: 52 | logger.error(mid_exception) 53 | return response.status_code, response.text 54 | else: 55 | return response.status_code, response.text 56 | except Exception as e: 57 | logger.exception(f"[-] Failed to query Bloodhound: \n{e}\n") 58 | raise Exception(f"[-] Failed to query Bloodhound: \n{e}\n") 59 | 60 | 61 | async def get_owned_id(taskData: PTTaskMessageAllData) -> int: 62 | global cachedAssetGroupOwnedID 63 | 64 | if cachedAssetGroupOwnedID is not None: 65 | return cachedAssetGroupOwnedID 66 | uri = f"/api/v2/asset-groups" 67 | try: 68 | response_code, response_data = await query_bloodhound(taskData, method='GET', uri=uri) 69 | if response_code == 200: 70 | asset_groups = response_data["data"] 71 | if len(asset_groups) == 0: 72 | raise Exception("no asset groups") 73 | for x in asset_groups["asset_groups"]: 74 | if x["system_group"] and x["tag"] == "owned": 75 | cachedAssetGroupOwnedID = x["id"] 76 | return cachedAssetGroupOwnedID 77 | raise Exception("no owned asset_group") 78 | raise Exception("Failed to query") 79 | except Exception as e: 80 | raise e 81 | 82 | 83 | async def get_whoami(taskData: PTTaskMessageAllData) -> str: 84 | uri = f"/api/v2/self" 85 | try: 86 | response_code, response_data = await query_bloodhound(taskData, method='GET', uri=uri) 87 | if response_code == 200: 88 | return response_data["data"]["id"] 89 | raise Exception("Failed to query self") 90 | except Exception as e: 91 | raise e 92 | 93 | 94 | async def get_saved_queries(taskData: PTTaskMessageAllData) -> list[dict]: 95 | try: 96 | user_id = await get_whoami(taskData=taskData) 97 | uri = f"/api/v2/saved-queries?sort_by=name&user_id={user_id}" 98 | response_code, response_data = await query_bloodhound(taskData, method='GET', uri=uri) 99 | if response_code == 200: 100 | return response_data["data"] 101 | raise Exception("Failed to query") 102 | except Exception as e: 103 | raise e 104 | 105 | 106 | async def process_standard_response(response_code: int, response_data: any, 107 | taskData: PTTaskMessageAllData, response: PTTaskCreateTaskingMessageResponse) -> \ 108 | PTTaskCreateTaskingMessageResponse: 109 | if 200 <= response_code < 300: 110 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 111 | TaskID=taskData.Task.ID, 112 | Response=json.dumps(response_data["data"]).encode("UTF8"), 113 | )) 114 | response.Success = True 115 | else: 116 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 117 | TaskID=taskData.Task.ID, 118 | Response=f"{response_data}".encode("UTF8"), 119 | )) 120 | response.TaskStatus = "Error: Bloodhound Query Error" 121 | return response 122 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher_saved.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherSavedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query_name", 13 | cli_name="query-name", 14 | description="Run a custom saved cypher query from Bloodhound CE", 15 | type=ParameterType.ChooseOne, 16 | dynamic_query_function=self.get_saved_queries, 17 | parameter_group_info=[ParameterGroupInfo( 18 | required=True, 19 | group_name="selected" 20 | )] 21 | ) 22 | ] 23 | 24 | async def get_saved_queries(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 25 | response = PTRPCDynamicQueryFunctionMessageResponse() 26 | payload_resp = await SendMythicRPCPayloadSearch(MythicRPCPayloadSearchMessage( 27 | PayloadUUID=callback.PayloadUUID 28 | )) 29 | if payload_resp.Success: 30 | if len(payload_resp.Payloads) == 0: 31 | await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( 32 | CallbackId=callback.Callback, 33 | Message=f"Failed to get payload: {payload_resp.Error}", 34 | MessageLevel="warning" 35 | )) 36 | response.Error = f"Failed to get payload: {payload_resp.Error}" 37 | return response 38 | payload = payload_resp.Payloads[0] 39 | fakeTaskData = PTTaskMessageAllData() 40 | fakeTaskData.BuildParameters = payload.BuildParameters 41 | fakeTaskData.Secrets = callback.Secrets 42 | choices = await BloodhoundAPI.get_saved_queries(taskData=fakeTaskData) 43 | response.Choices = [x["name"] for x in choices] 44 | response.Success = True 45 | return response 46 | else: 47 | await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( 48 | CallbackId=callback.Callback, 49 | Message=f"Failed to get payload: {payload_resp.Error}", 50 | MessageLevel="warning" 51 | )) 52 | response.Error = f"Failed to get payload: {payload_resp.Error}" 53 | return response 54 | 55 | async def parse_arguments(self): 56 | return self.load_args_from_json_string(self.command_line) 57 | 58 | async def parse_dictionary(self, dictionary_arguments): 59 | return self.load_args_from_dictionary(dictionary=dictionary_arguments) 60 | 61 | 62 | class CypherSaved(CommandBase): 63 | cmd = "cypher_saved" 64 | needs_admin = False 65 | help_cmd = cmd 66 | description = "Run one of your saved queries from Bloodhound" 67 | version = 1 68 | author = "@its_a_feature_" 69 | argument_class = CypherSavedArguments 70 | browser_script = BrowserScript(script_name="cypher", author="@its_a_feature_", for_new_ui=True) 71 | attackmapping = [] 72 | 73 | async def create_go_tasking(self, 74 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 75 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 76 | TaskID=taskData.Task.ID, 77 | Success=False, 78 | Completed=True, 79 | DisplayParams=f"{taskData.args.get_arg('query_name')}" 80 | ) 81 | saved_queries = await BloodhoundAPI.get_saved_queries(taskData=taskData) 82 | matched_query = [x["query"] for x in saved_queries if x["name"] == taskData.args.get_arg("query_name")] 83 | if len(matched_query) == 0: 84 | response.TaskStatus = "error: Saved Query Not Found" 85 | return response 86 | uri = f"/api/v2/graphs/cypher" 87 | body = json.dumps({ 88 | "include_properties": True, 89 | "query": matched_query[0] 90 | }).encode() 91 | response.Stdout = matched_query[0] 92 | try: 93 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 94 | method='POST', 95 | body=body, 96 | uri=uri) 97 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 98 | response_data=response_data, 99 | taskData=taskData, 100 | response=response) 101 | 102 | except Exception as e: 103 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 104 | TaskID=taskData.Task.ID, 105 | Response=f"{e}".encode("UTF8"), 106 | )) 107 | response.TaskStatus = "Error: Bloodhound Access Error" 108 | return response 109 | 110 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 111 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 112 | return resp 113 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # Pulled from Thanatos (https://github.com/MythicAgents/thanatos/blob/rewrite/.github/workflows/image.yml) - MEhrn00 2 | 3 | # Name for the Github actions workflow 4 | name: Build and push container images 5 | 6 | on: 7 | # Only run workflow when there is a new release published in Github 8 | #release: 9 | # types: [published] 10 | push: 11 | branches: 12 | - 'master' 13 | - 'main' 14 | tags: 15 | - "v*.*.*" 16 | 17 | # Variables holding configuration settings 18 | env: 19 | # Container registry the built container image will be pushed to 20 | REGISTRY: ghcr.io 21 | 22 | # Set the container image name to the Github repository name. (MythicAgents/apfell) 23 | AGENT_IMAGE_NAME: ${{ github.repository }} 24 | 25 | # Description label for the package in Github 26 | IMAGE_DESCRIPTION: ${{ github.repository }} container for use with Mythic 27 | 28 | # Source URL for the package in Github. This links the Github repository packages list 29 | # to this container image 30 | IMAGE_SOURCE: ${{ github.server_url }}/${{ github.repository }} 31 | 32 | # License for the container image 33 | IMAGE_LICENSE: BSD-3-Clause 34 | 35 | # Set the container image version to the Github release tag 36 | VERSION: ${{ github.ref_name }} 37 | #VERSION: ${{ github.event.head_commit.message }} 38 | 39 | RELEASE_BRANCH: main 40 | 41 | jobs: 42 | # Builds the base container image and pushes it to the container registry 43 | agent_build: 44 | runs-on: ubuntu-latest 45 | permissions: 46 | contents: write 47 | packages: write 48 | steps: 49 | - name: Checkout the repository 50 | uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout 51 | - name: Log in to the container registry 52 | uses: docker/login-action@v3 # ref: https://github.com/marketplace/actions/docker-login 53 | with: 54 | registry: ${{ env.REGISTRY }} 55 | username: ${{ github.actor }} 56 | password: ${{ secrets.GITHUB_TOKEN }} 57 | - name: Set up QEMU 58 | uses: docker/setup-qemu-action@v2 59 | with: 60 | platforms: 'arm64,arm' 61 | - name: Set up Docker Buildx 62 | id: buildx 63 | uses: docker/setup-buildx-action@v2 64 | # the following are unique to this job 65 | - name: Lowercase the server container image name 66 | run: echo "AGENT_IMAGE_NAME=${AGENT_IMAGE_NAME,,}" >> ${GITHUB_ENV} 67 | - name: Build and push the server container image 68 | uses: docker/build-push-action@v5 # ref: https://github.com/marketplace/actions/build-and-push-docker-images 69 | with: 70 | context: Payload_Type/bloodhound 71 | file: Payload_Type/bloodhound/.docker/Dockerfile 72 | tags: | 73 | ${{ env.REGISTRY }}/${{ env.AGENT_IMAGE_NAME }}:${{ env.VERSION }} 74 | ${{ env.REGISTRY }}/${{ env.AGENT_IMAGE_NAME }}:latest 75 | push: ${{ github.ref_type == 'tag' }} 76 | # These container metadata labels allow configuring the package in Github 77 | # packages. The source will link the package to this Github repository 78 | labels: | 79 | org.opencontainers.image.source=${{ env.IMAGE_SOURCE }} 80 | org.opencontainers.image.description=${{ env.IMAGE_DESCRIPTION }} 81 | org.opencontainers.image.licenses=${{ env.IMAGE_LICENSE }} 82 | platforms: linux/amd64,linux/arm64 83 | 84 | update_files: 85 | runs-on: ubuntu-latest 86 | needs: 87 | - agent_build 88 | permissions: 89 | contents: write 90 | packages: write 91 | 92 | steps: 93 | # Pull in the repository code 94 | - name: Checkout the repository 95 | uses: actions/checkout@v4 # ref: https://github.com/marketplace/actions/checkout 96 | 97 | # update names to lowercase 98 | - name: Lowercase the container image name 99 | run: echo "AGENT_IMAGE_NAME=${AGENT_IMAGE_NAME,,}" >> ${GITHUB_ENV} 100 | 101 | # The Dockerfile which Mythic uses to pull in the base container image needs to be 102 | # updated to reference the newly built container image 103 | - name: Fix the server Dockerfile reference to reference the new release tag 104 | working-directory: Payload_Type/bloodhound 105 | run: | 106 | sed -i "s|^FROM ghcr\.io.*$|FROM ${REGISTRY}/${AGENT_IMAGE_NAME}:${VERSION}|" Dockerfile 107 | 108 | - name: Update package.json version 109 | uses: jossef/action-set-json-field@v2.1 110 | with: 111 | file: config.json 112 | field: remote_images.bloodhound 113 | value: ${{env.REGISTRY}}/${{env.AGENT_IMAGE_NAME}}:${{env.VERSION}} 114 | 115 | # Push the changes to the Dockerfile 116 | - name: Push the updated base Dockerfile image reference changes 117 | if: ${{ github.ref_type == 'tag' }} 118 | uses: EndBug/add-and-commit@v9 # ref: https://github.com/marketplace/actions/add-commit 119 | with: 120 | # Only add the Dockerfile changes. Nothing else should have been modified 121 | add: "['Payload_Type/bloodhound/Dockerfile', 'config.json']" 122 | # Use the Github actions bot for the commit author 123 | default_author: github_actions 124 | committer_email: github-actions[bot]@users.noreply.github.com 125 | 126 | # Set the commit message 127 | message: "Bump Dockerfile tag to match release '${{ env.VERSION }}'" 128 | 129 | # Overwrite the current git tag with the new changes 130 | tag: '${{ env.VERSION }} --force' 131 | 132 | # Push the new changes with the tag overwriting the current one 133 | tag_push: '--force' 134 | 135 | # Push the commits to the branch marked as the release branch 136 | push: origin HEAD:${{ env.RELEASE_BRANCH }} --set-upstream 137 | 138 | # Have the workflow fail in case there are pathspec issues 139 | pathspec_error_handling: exitImmediately 140 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/get_user.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class GetUserArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="object_id", 13 | description="Which User object_id to query", 14 | type=ParameterType.String, 15 | parameter_group_info=[ParameterGroupInfo( 16 | required=True, 17 | ui_position=1, 18 | group_name="object_id" 19 | )] 20 | ), 21 | CommandParameter( 22 | name="name", 23 | description="Which User search for", 24 | type=ParameterType.String, 25 | parameter_group_info=[ParameterGroupInfo( 26 | required=True, 27 | ui_position=1, 28 | group_name="search" 29 | )] 30 | ), 31 | 32 | ] 33 | 34 | async def parse_arguments(self): 35 | if len(self.command_line) == 0: 36 | raise ValueError("Must supply an object_id") 37 | self.add_arg("object_id", self.command_line) 38 | 39 | async def parse_dictionary(self, dictionary_arguments): 40 | self.load_args_from_dictionary(dictionary=dictionary_arguments) 41 | 42 | 43 | async def finished_searching(completionMsg: PTTaskCompletionFunctionMessage) -> PTTaskCompletionFunctionMessageResponse: 44 | response = PTTaskCompletionFunctionMessageResponse(Success=True) 45 | if "error" in completionMsg.SubtaskData.Task.Status: 46 | response.TaskStatus = "error: Failed to find object" 47 | response.Success = False 48 | response.Completed = True 49 | return response 50 | subtaskOutput = await SendMythicRPCResponseSearch(MythicRPCResponseSearchMessage( 51 | TaskID=completionMsg.SubtaskData.Task.ID 52 | )) 53 | if not subtaskOutput.Success: 54 | response.TaskStatus = "error: Failed to get search output" 55 | response.Success = False 56 | response.Completed = True 57 | return response 58 | json.loads(subtaskOutput.Responses[0].Response) 59 | return response 60 | 61 | 62 | class GetUser(CommandBase): 63 | cmd = "get_user" 64 | needs_admin = False 65 | help_cmd = "get_user -object_id \"S-1-5-21-909015691-3030120388-2582151266-512\"" 66 | description = "Get information about a specific user" 67 | version = 1 68 | author = "@its_a_feature_" 69 | argument_class = GetUserArguments 70 | supported_ui_features = ["bloodhound:get_user"] 71 | #browser_script = BrowserScript(script_name="controllables", author="@its_a_feature_", for_new_ui=True) 72 | attackmapping = [] 73 | completion_functions = { 74 | "finished_searching": finished_searching 75 | } 76 | 77 | async def create_go_tasking(self, 78 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 79 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 80 | TaskID=taskData.Task.ID, 81 | Success=False, 82 | Completed=True, 83 | DisplayParams=f" for {taskData.args.get_arg('object_id')}" 84 | ) 85 | if taskData.args.get_parameter_group_name() == "object_id": 86 | uri = f"/api/v2/users/{taskData.args.get_arg('object_id')}" 87 | try: 88 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 89 | method='GET', 90 | uri=uri) 91 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 92 | response_data=response_data, 93 | taskData=taskData, 94 | response=response) 95 | 96 | except Exception as e: 97 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 98 | TaskID=taskData.Task.ID, 99 | Response=f"{e}".encode("UTF8"), 100 | )) 101 | response.TaskStatus = "Error: Bloodhound Access Error" 102 | return response 103 | else: 104 | # we need to search for the user first 105 | subtask = await SendMythicRPCTaskCreateSubtask(MythicRPCTaskCreateSubtaskMessage( 106 | TaskID=taskData.Task.ID, 107 | SubtaskCallbackFunction="finished_searching", 108 | CommandName="search", 109 | Params=json.dumps({"query": taskData.args.get_arg("name")}) 110 | )) 111 | if subtask.Success: 112 | return MythicCommandBase.PTTaskCreateTaskingMessageResponse( 113 | TaskID=taskData.Task.ID, 114 | Success=True, 115 | Completed=False, 116 | DisplayParams=f" for {taskData.args.get_arg('name')}" 117 | ) 118 | else: 119 | MythicCommandBase.PTTaskCreateTaskingMessageResponse( 120 | TaskID=taskData.Task.ID, 121 | Success=False, 122 | Completed=True, 123 | DisplayParams=f" for {taskData.args.get_arg('object_id')}", 124 | Error=subtask.Error 125 | ) 126 | 127 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 128 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 129 | return resp 130 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/browser_scripts/cypher.js: -------------------------------------------------------------------------------- 1 | function(task, responses){ 2 | function colorMapping(kind){ 3 | const round = {"border": "3px solid black", 4 | "color": "#000000", 5 | "borderRadius":"50%"}; 6 | switch(kind){ 7 | case "Group": 8 | return {...round, "backgroundColor": "rgb(210, 228, 22)"}; 9 | case "User": 10 | return {...round, "backgroundColor": "rgb(36, 230, 29)"}; 11 | case "Computer": 12 | return {...round, "backgroundColor": "rgb(221, 98, 96)"}; 13 | case "Container": 14 | return {...round, "backgroundColor": "rgb(242, 135, 101)"}; 15 | case "Domain": 16 | return {...round, "backgroundColor": "rgb(35, 228, 170)"}; 17 | case "OU": 18 | return {...round, "backgroundColor": "rgb(253, 155, 10)"}; 19 | case "GPO": 20 | return {...round, "backgroundColor": "rgb(134, 116, 253)"}; 21 | case "TierZero": 22 | return {"border": "2px solid black", "borderRadius": "50%", "backgroundColor": "black", "color": "white"}; 23 | case "Owned": 24 | return {"border": "2px solid black", "borderRadius": "50%", "backgroundColor": "black", "color": "red"}; 25 | default: 26 | return {...round, "backgroundColor": "white", "color": "black"}; 27 | } 28 | } 29 | function iconMapping(kind){ 30 | switch(kind){ 31 | case "Group": 32 | return "group"; 33 | case "User": 34 | return "user"; 35 | case "Computer": 36 | return "computer"; 37 | case "Container": 38 | return "container"; 39 | case "Domain": 40 | return "language"; 41 | case "OU": 42 | return "lan"; 43 | case "GPO": 44 | return "list"; 45 | case "TierZero": 46 | return "diamond"; 47 | case "Owned": 48 | return "skull"; 49 | case "overlay": 50 | return ""; 51 | default: 52 | return "help"; 53 | } 54 | } 55 | function edgeColor(kind){ 56 | switch(kind){ 57 | case "MemberOf": 58 | case "Contains": 59 | case "TrustedBy": 60 | return "success"; 61 | case "GenericAll": 62 | case "GenericWrite": 63 | case "HasSession": 64 | case "WriteDacl": 65 | case "AddKeyCredentialLink": 66 | case "WriteOwner": 67 | return "warning"; 68 | default: 69 | return "info"; 70 | } 71 | } 72 | function getAnimateEdge(kind){ 73 | if(edgeColor(kind) === "success"){ 74 | return true; 75 | } 76 | return false; 77 | } 78 | if(task.status.includes("error")){ 79 | const combined = responses.reduce( (prev, cur) => { 80 | return prev + cur; 81 | }, ""); 82 | return {'plaintext': combined}; 83 | }else if(task.completed){ 84 | if(responses.length > 0){ 85 | try{ 86 | let data = JSON.parse(responses[0]); 87 | let nodes = []; 88 | for(const [key, val] of Object.entries(data["nodes"])){ 89 | let tier0 = val?.["properties"]?.["system_tags"]?.includes("admin_tier_0") || false; 90 | if(!tier0){ 91 | tier0 = val?.["isTierZero"] || false; 92 | } 93 | let owned = val?.["properties"]?.["system_tags"]?.includes("owned") || false; 94 | let overlayImg = "overlay"; 95 | if(tier0){overlayImg = "TierZero"} 96 | if(owned){overlayImg = "Owned"} 97 | let buttons = [ 98 | { 99 | "name": "Mark As Owned", 100 | "type": "task", 101 | "ui_feature": "bloodhound:mark_owned", 102 | "parameters": {"object_id": val["objectId"]}, 103 | "startIcon": "kill" 104 | }, 105 | { 106 | "name": "Shortest Path To Here", 107 | "type": "task", 108 | "ui_feature": "bloodhound:shortest_path", 109 | openDialog: true, 110 | "parameters": {"end_node": val["objectId"]}, 111 | }, 112 | { 113 | "name": "Shortest Path From Here", 114 | "type": "task", 115 | "ui_feature": "bloodhound:shortest_path", 116 | openDialog: true, 117 | "parameters": {"start_node": val["objectId"]}, 118 | } 119 | ]; 120 | if(val?.properties?.hasspn && val?.properties?.serviceprincipalnames?.length > 0){ 121 | val?.properties?.serviceprincipalnames.forEach( (v) => { 122 | buttons.push( 123 | { 124 | "name": "Kerberoast " + v, 125 | "type": "task", 126 | "ui_feature": "bloodhound:kerberoast", 127 | "parameters": { 128 | "serviceprincipalname": v, 129 | }, 130 | selectCallback: true, 131 | openDialog: true, 132 | } 133 | ) 134 | }) 135 | 136 | } 137 | nodes.push({ 138 | id: key, 139 | img: iconMapping(val["kind"]), 140 | style: colorMapping(val["kind"]), 141 | overlay_img: iconMapping(overlayImg), 142 | overlay_style: colorMapping(overlayImg), 143 | data: {...val }, 144 | buttons: buttons 145 | }) 146 | } 147 | let edges = data["edges"].map( e => { 148 | let sData = data["nodes"][e["source"]]; 149 | sData["id"] = e["source"]; 150 | let dData = data["nodes"][e["target"]]; 151 | dData["id"] = e["target"]; 152 | return { 153 | source: sData, 154 | destination: dData, 155 | label: e["label"], 156 | data: { 157 | kind: e["kind"], 158 | lastSeen: e["lastSeen"], 159 | }, 160 | animate: getAnimateEdge(e["kind"]), 161 | color: edgeColor(e["kind"]), 162 | buttons: [ 163 | { 164 | "name": "Abuse Edge", 165 | "type": "task", 166 | "ui_feature": `bloodhound:${e['kind']}`.toLowerCase(), 167 | "parameters": {"edge": e["kind"], "source": sData["label"], "destination": dData["label"]}, 168 | "startIcon": "kill", 169 | selectCallback: true, 170 | openDialog: true, 171 | } 172 | ] 173 | } 174 | }); 175 | 176 | return { 177 | "graph": { 178 | nodes: nodes, 179 | edges: edges, 180 | group_by: "" 181 | } 182 | } 183 | }catch(error){ 184 | console.log(error); 185 | const combined = responses.reduce( (prev, cur) => { 186 | return prev + cur; 187 | }, ""); 188 | return {'plaintext': combined}; 189 | } 190 | }else{ 191 | return {"plaintext": "No output from command"}; 192 | } 193 | }else{ 194 | return {"plaintext": "No data to display..."}; 195 | } 196 | } -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/upload.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class UploadArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="file", cli_name="new-file", display_name="File to upload", type=ParameterType.File, 13 | description="Select new file to upload", 14 | parameter_group_info=[ 15 | ParameterGroupInfo( 16 | required=True, 17 | group_name="Default", 18 | ui_position=0 19 | ) 20 | ] 21 | ), 22 | CommandParameter( 23 | name="filename", display_name="Filename within Mythic", 24 | description="Supply existing filename in Mythic to upload", 25 | type=ParameterType.ChooseOne, 26 | dynamic_query_function=self.get_files, 27 | parameter_group_info=[ 28 | ParameterGroupInfo( 29 | required=True, 30 | ui_position=0, 31 | group_name="specify already uploaded file by name" 32 | ) 33 | ] 34 | ), 35 | 36 | ] 37 | 38 | async def parse_arguments(self): 39 | if len(self.command_line) == 0: 40 | raise ValueError("Must supply arguments") 41 | raise ValueError("Must supply named arguments or use the modal") 42 | 43 | async def parse_dictionary(self, dictionary_arguments): 44 | self.load_args_from_dictionary(dictionary_arguments) 45 | 46 | async def get_files(self, callback: PTRPCDynamicQueryFunctionMessage) -> PTRPCDynamicQueryFunctionMessageResponse: 47 | response = PTRPCDynamicQueryFunctionMessageResponse() 48 | file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 49 | CallbackID=callback.Callback, 50 | LimitByCallback=False, 51 | IsDownloadFromAgent=True, 52 | IsScreenshot=False, 53 | IsPayload=False, 54 | Filename="", 55 | )) 56 | if file_resp.Success: 57 | file_names = [] 58 | for f in file_resp.Files: 59 | if f.Filename not in file_names: 60 | file_names.append(f.Filename) 61 | response.Success = True 62 | response.Choices = file_names 63 | return response 64 | else: 65 | await SendMythicRPCOperationEventLogCreate(MythicRPCOperationEventLogCreateMessage( 66 | CallbackId=callback.Callback, 67 | Message=f"Failed to get files: {file_resp.Error}", 68 | MessageLevel="warning" 69 | )) 70 | response.Error = f"Failed to get files: {file_resp.Error}" 71 | return response 72 | 73 | 74 | class Upload(CommandBase): 75 | cmd = "upload" 76 | needs_admin = False 77 | help_cmd = "upload -filename bloodhound.zip" 78 | description = "Upload a file into Bloodhound for processing" 79 | version = 1 80 | author = "@its_a_feature_" 81 | argument_class = UploadArguments 82 | supported_ui_features = ["bloodhound:upload"] 83 | attackmapping = [] 84 | 85 | async def create_go_tasking(self, 86 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 87 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 88 | TaskID=taskData.Task.ID, 89 | Success=False, 90 | Completed=True, 91 | ) 92 | uri = f"/api/v2/file-upload/start" 93 | try: 94 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 95 | method='POST', 96 | uri=uri) 97 | if response_code != 201: 98 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 99 | response_data=response_data, 100 | taskData=taskData, 101 | response=response) 102 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 103 | TaskID=taskData.Task.ID, 104 | Response=f"Starting file upload with ID: {response_data['data']['id']}\n".encode("UTF8") 105 | )) 106 | fileUploadURI = f"/api/v2/file-upload/{response_data['data']['id']}" 107 | filename, fileContents = await self.get_file_contents(taskData) 108 | if len(fileContents) == 0: 109 | response.TaskStatus = "Error: Failed to get file contents" 110 | return response 111 | response.DisplayParams = filename 112 | upload_response_code, upload_response_data = await BloodhoundAPI.query_bloodhound(taskData, 113 | method="POST", 114 | uri=fileUploadURI, 115 | body=fileContents) 116 | if upload_response_code != 202: 117 | return await BloodhoundAPI.process_standard_response(response_code=upload_response_code, 118 | response_data=upload_response_data, 119 | taskData=taskData, 120 | response=response) 121 | finalURI = f"{fileUploadURI}/end" 122 | final_response_code, final_response_data = await BloodhoundAPI.query_bloodhound(taskData, 123 | method="POST", 124 | uri=finalURI) 125 | if final_response_code == 200: 126 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 127 | TaskID=taskData.Task.ID, 128 | Response=f"Successfully uploaded file and ended stream".encode("UTF8"), 129 | )) 130 | response.Success = True 131 | return response 132 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 133 | TaskID=taskData.Task.ID, 134 | Response=f"Failed to finish final transfer: {final_response_data}".encode("UTF8"), 135 | )) 136 | 137 | except Exception as e: 138 | logger.exception(e) 139 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 140 | TaskID=taskData.Task.ID, 141 | Response=f"{e}".encode("UTF8"), 142 | )) 143 | response.TaskStatus = "Error: Bloodhound Access Error" 144 | return response 145 | 146 | async def get_file_contents(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> (str, bytes): 147 | groupName = taskData.args.get_parameter_group_name() 148 | if groupName == "Default": 149 | filename_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 150 | TaskID=taskData.Task.ID, 151 | AgentFileID=taskData.args.get_arg("file"), 152 | )) 153 | if filename_resp.Success: 154 | if len(filename_resp.Files) > 0: 155 | 156 | file_resp = await SendMythicRPCFileGetContent(MythicRPCFileGetContentMessage( 157 | AgentFileId=taskData.args.get_arg("file") 158 | )) 159 | if not file_resp.Success: 160 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 161 | TaskID=taskData.Task.ID, 162 | Response=f"{file_resp.Error}".encode("UTF8") 163 | )) 164 | return filename_resp.Files[0].Filename, b"" 165 | return filename_resp.Files[0].Filename, file_resp.Content 166 | return "", b"" 167 | return "", b"" 168 | file_resp = await SendMythicRPCFileSearch(MythicRPCFileSearchMessage( 169 | TaskID=taskData.Task.ID, 170 | Filename=taskData.args.get_arg("filename"), 171 | LimitByCallback=False, 172 | MaxResults=1, 173 | IsDownloadFromAgent=True, 174 | IsScreenshot=False, 175 | IsPayload=False, 176 | )) 177 | if file_resp.Success: 178 | if len(file_resp.Files) > 0: 179 | old_file_resp = await SendMythicRPCFileGetContent(MythicRPCFileGetContentMessage( 180 | AgentFileId=file_resp.Files[0].AgentFileId 181 | )) 182 | return taskData.args.get_arg("filename"), old_file_resp.Content 183 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 184 | TaskID=taskData.Task.ID, 185 | Response=f"Failed to find that file by name, no results".encode("UTF8") 186 | )) 187 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 188 | TaskID=taskData.Task.ID, 189 | Response=f"{file_resp.Error}".encode("UTF8") 190 | )) 191 | return taskData.args.get_arg("filename"), b"" 192 | 193 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 194 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 195 | return resp 196 | -------------------------------------------------------------------------------- /agent_icons/bloodhound.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/bloodhound.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | 22 | 23 | 24 | 25 | 27 | 28 | -------------------------------------------------------------------------------- /Payload_Type/bloodhound/bloodhound/agent_functions/cypher_predefined.py: -------------------------------------------------------------------------------- 1 | from mythic_container.MythicCommandBase import * 2 | from mythic_container.MythicRPC import * 3 | from bloodhound.BloodhoundRequests import BloodhoundAPI 4 | 5 | 6 | class CypherPredefinedArguments(TaskArguments): 7 | 8 | def __init__(self, command_line, **kwargs): 9 | super().__init__(command_line, **kwargs) 10 | self.args = [ 11 | CommandParameter( 12 | name="query_name", 13 | cli_name="query-name", 14 | description="Run a predefined cypher query from Bloodhound CE", 15 | type=ParameterType.ChooseOne, 16 | default_value="All Domain Admins", 17 | choices=list(predefined_queries.keys()), 18 | parameter_group_info=[ParameterGroupInfo( 19 | required=True 20 | )] 21 | ) 22 | ] 23 | 24 | async def parse_arguments(self): 25 | return self.load_args_from_json_string(self.command_line) 26 | 27 | async def parse_dictionary(self, dictionary_arguments): 28 | return self.load_args_from_dictionary(dictionary=dictionary_arguments) 29 | 30 | 31 | predefined_queries = { 32 | "All Domain Admins": """ 33 | MATCH p = (t:Group)<-[:MemberOf*1..]-(a) 34 | WHERE (a:User or a:Computer) and t.objectid ENDS WITH '-512' 35 | RETURN p 36 | LIMIT 1000 37 | """, 38 | "Map domain trusts": """ 39 | MATCH p = (:Domain)-[:TrustedBy]->(:Domain) 40 | RETURN p 41 | LIMIT 1000 42 | """, 43 | "Locations of Tier Zero / High Value objects": """ 44 | MATCH p = (t:Base)<-[:Contains*1..]-(:Domain) 45 | WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' 46 | RETURN p 47 | LIMIT 1000 48 | """, 49 | "Map OU structure": """ 50 | MATCH p = (:Domain)-[:Contains*1..]->(:OU) 51 | RETURN p 52 | LIMIT 1000 53 | """, 54 | "Principals with DCSync privileges": """ 55 | MATCH p=(:Base)-[:DCSync|AllExtendedRights|GenericAll]->(:Domain) 56 | RETURN p 57 | LIMIT 1000 58 | """, 59 | "Principals with foreign domain group membership": """ 60 | MATCH p=(s:Base)-[:MemberOf]->(t:Group) 61 | WHERE s.domainsid<>t.domainsid 62 | RETURN p 63 | LIMIT 1000 64 | """, 65 | "Computers where Domain Users are local administrators": """ 66 | MATCH p=(s:Group)-[:AdminTo]->(:Computer) 67 | WHERE s.objectid ENDS WITH '-513' 68 | RETURN p 69 | LIMIT 1000 70 | """, 71 | "Computers where Domain Users can read LAPS passwords": """ 72 | MATCH p=(s:Group)-[:AllExtendedRights|ReadLAPSPassword]->(:Computer) 73 | WHERE s.objectid ENDS WITH '-513' 74 | RETURN p 75 | LIMIT 1000 76 | """, 77 | "Paths from Domain Users to Tier Zero / High Value targets": """ 78 | MATCH p=shortestPath((s:Group)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t)) 79 | WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' AND s.objectid ENDS WITH '-513' AND s<>t 80 | RETURN p 81 | LIMIT 1000 82 | """, 83 | "Workstations where Domain Users can RDP": """ 84 | MATCH p=(s:Group)-[:CanRDP]->(t:Computer) 85 | WHERE s.objectid ENDS WITH '-513' AND NOT toUpper(t.operatingsystem) CONTAINS 'SERVER' 86 | RETURN p 87 | LIMIT 1000 88 | """, 89 | "Servers where Domain Users can RDP": """ 90 | MATCH p=(s:Group)-[:CanRDP]->(t:Computer) 91 | WHERE s.objectid ENDS WITH '-513' AND toUpper(t.operatingsystem) CONTAINS 'SERVER' 92 | RETURN p 93 | LIMIT 1000 94 | """, 95 | "Dangerous privileges for Domain Users groups": """ 96 | MATCH p=(s:Group)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy]->(:Base) 97 | WHERE s.objectid ENDS WITH '-513' 98 | RETURN p 99 | LIMIT 1000 100 | """, 101 | "Domain Admins logons to non-Domain Controllers": """ 102 | MATCH (s)-[:MemberOf*0..]->(g:Group) 103 | WHERE g.objectid ENDS WITH '-516' 104 | WITH COLLECT(s) AS exclude 105 | MATCH p = (c:Computer)-[:HasSession]->(:User)-[:MemberOf*1..]->(g:Group) 106 | WHERE g.objectid ENDS WITH '-512' AND NOT c IN exclude 107 | RETURN p 108 | LIMIT 1000 109 | """, 110 | "Kerberoastable members of Tier Zero / High Value groups": """ 111 | MATCH (u:User) 112 | WHERE u.hasspn=true 113 | AND u.enabled = true 114 | AND NOT u.objectid ENDS WITH '-502' 115 | AND NOT COALESCE(u.gmsa, false) = true 116 | AND NOT COALESCE(u.msa, false) = true 117 | AND COALESCE(u.system_tags, '') CONTAINS 'admin_tier_0' 118 | RETURN u 119 | LIMIT 100 120 | """, 121 | "All Kerberoastable users": """ 122 | MATCH (u:User) 123 | WHERE u.hasspn=true 124 | AND u.enabled = true 125 | AND NOT u.objectid ENDS WITH '-502' 126 | AND NOT COALESCE(u.gmsa, false) = true 127 | AND NOT COALESCE(u.msa, false) = true 128 | RETURN u 129 | LIMIT 100 130 | """, 131 | "Kerberoastable users with most privileges": """ 132 | MATCH (u:User) 133 | WHERE u.hasspn = true 134 | AND u.enabled = true 135 | AND NOT u.objectid ENDS WITH '-502' 136 | AND NOT COALESCE(u.gmsa, false) = true 137 | AND NOT COALESCE(u.msa, false) = true 138 | MATCH (u)-[:MemberOf|AdminTo*1..]->(c:Computer) 139 | WITH DISTINCT u, COUNT(c) AS adminCount 140 | RETURN u 141 | ORDER BY adminCount DESC 142 | LIMIT 100 143 | """, 144 | "AS-REP Roastable users (DontReqPreAuth)": """ 145 | MATCH (u:User) 146 | WHERE u.dontreqpreauth = true 147 | AND u.enabled = true 148 | RETURN u 149 | LIMIT 100 150 | """, 151 | "Shortest paths to systems trusted for unconstrained delegation": """ 152 | MATCH p=shortestPath((s)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t:Computer)) 153 | WHERE t.unconstraineddelegation = true AND s<>t 154 | RETURN p 155 | LIMIT 1000 156 | """, 157 | "Shortest paths to Domain Admins from Kerberoastable users": """ 158 | MATCH p=shortestPath((s:User)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t:Group)) 159 | WHERE s.hasspn=true 160 | AND s.enabled = true 161 | AND NOT s.objectid ENDS WITH '-502' 162 | AND NOT COALESCE(s.gmsa, false) = true 163 | AND NOT COALESCE(s.msa, false) = true 164 | AND t.objectid ENDS WITH '-512' 165 | RETURN p 166 | LIMIT 1000 167 | """, 168 | "Shortest paths to Tier Zero / High Value targets": """ 169 | MATCH p=shortestPath((s)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t)) 170 | WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' AND s<>t 171 | RETURN p 172 | LIMIT 1000 173 | """, 174 | "Shortest paths from Domain Users to Tier Zero / High Value targets": """ 175 | MATCH p=shortestPath((s:Group)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t)) 176 | WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' AND s.objectid ENDS WITH '-513' AND s<>t 177 | RETURN p 178 | LIMIT 1000 179 | """, 180 | "Shortest paths to Domain Admins": """ 181 | MATCH p=shortestPath((t:Group)<-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]-(s:Base)) 182 | WHERE t.objectid ENDS WITH '-512' AND s<>t 183 | RETURN p 184 | LIMIT 1000 185 | """, 186 | "Shortest paths from Owned objects": """ 187 | MATCH p=shortestPath((s:Base)-[:Owns|GenericAll|GenericWrite|WriteOwner|WriteDacl|MemberOf|ForceChangePassword|AllExtendedRights|AddMember|HasSession|GPLink|AllowedToDelegate|CoerceToTGT|AllowedToAct|AdminTo|CanPSRemote|CanRDP|ExecuteDCOM|HasSIDHistory|AddSelf|DCSync|ReadLAPSPassword|ReadGMSAPassword|DumpSMSAPassword|SQLAdmin|AddAllowedToAct|WriteSPN|AddKeyCredentialLink|SyncLAPSPassword|WriteAccountRestrictions|WriteGPLink|GoldenCert|ADCSESC1|ADCSESC3|ADCSESC4|ADCSESC6a|ADCSESC6b|ADCSESC9a|ADCSESC9b|ADCSESC10a|ADCSESC10b|ADCSESC13|SyncedToEntraUser|CoerceAndRelayNTLMToSMB|CoerceAndRelayNTLMToADCS|WriteOwnerLimitedRights|OwnsLimitedRights|CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|Contains|DCFor|TrustedBy*1..]->(t:Base)) 188 | WHERE COALESCE(s.system_tags, '') CONTAINS 'owned' AND s<>t 189 | RETURN p 190 | LIMIT 1000 191 | """, 192 | "PKI hierarchy": """ 193 | MATCH p=()-[:HostsCAService|IssuedSignedBy|EnterpriseCAFor|RootCAFor|TrustedForNTAuth|NTAuthStoreFor*..]->(:Domain) 194 | RETURN p 195 | LIMIT 1000 196 | """, 197 | "Public Key Services container": """ 198 | MATCH p = (c:Container)-[:Contains*..]->(:Base) 199 | WHERE c.distinguishedname starts with 'CN=PUBLIC KEY SERVICES,CN=SERVICES,CN=CONFIGURATION,DC=' 200 | RETURN p 201 | LIMIT 1000 202 | """, 203 | "Enrollment rights on published certificate templates": """ 204 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(:CertTemplate)-[:PublishedTo]->(:EnterpriseCA) 205 | RETURN p 206 | LIMIT 1000 207 | """, 208 | "Enrollment rights on published ESC1 certificate templates": """ 209 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA) 210 | WHERE ct.enrolleesuppliessubject = True 211 | AND ct.authenticationenabled = True 212 | AND ct.requiresmanagerapproval = False 213 | AND (ct.authorizedsignatures = 0 OR ct.schemaversion = 1) 214 | RETURN p 215 | LIMIT 1000 216 | """, 217 | "Enrollment rights on published ESC2 certificate templates": """ 218 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(c:CertTemplate)-[:PublishedTo]->(:EnterpriseCA) 219 | WHERE c.requiresmanagerapproval = false 220 | AND (c.effectiveekus = [''] OR '2.5.29.37.0' IN c.effectiveekus) 221 | AND (c.authorizedsignatures = 0 OR c.schemaversion = 1) 222 | RETURN p 223 | LIMIT 1000 224 | """, 225 | "Enrollment rights on published enrollment agent certificate templates": """ 226 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA) 227 | WHERE '1.3.6.1.4.1.311.20.2.1' IN ct.effectiveekus 228 | OR '2.5.29.37.0' IN ct.effectiveekus 229 | OR SIZE(ct.effectiveekus) = 0 230 | RETURN p 231 | LIMIT 1000 232 | """, 233 | "Enrollment rights on published certificate templates with no security extension": """ 234 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(:EnterpriseCA) 235 | WHERE ct.nosecurityextension = true 236 | RETURN p 237 | LIMIT 1000 238 | """, 239 | "Enrollment rights on certificate templates published to Enterprise CA with User Specified SAN enabled": """ 240 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(ct:CertTemplate)-[:PublishedTo]->(eca:EnterpriseCA) 241 | WHERE eca.isuserspecifiessanenabled = True 242 | RETURN p 243 | LIMIT 1000 244 | """, 245 | "CA administrators and CA managers": """ 246 | MATCH p = (:Base)-[:ManageCertificates|ManageCA]->(:EnterpriseCA) 247 | RETURN p 248 | LIMIT 1000 249 | """, 250 | "Domain controllers with weak certificate binding enabled": """ 251 | MATCH p = (s:Computer)-[:DCFor]->(:Domain) 252 | WHERE s.strongcertificatebindingenforcementraw = 0 OR s.strongcertificatebindingenforcementraw = 1 253 | RETURN p 254 | LIMIT 1000 255 | """, 256 | "Domain controllers with UPN certificate mapping enabled": """ 257 | MATCH p = (s:Computer)-[:DCFor]->(:Domain) 258 | WHERE s.certificatemappingmethodsraw IN [4, 5, 6, 7, 12, 13, 14, 15, 20, 21, 22, 23, 28, 29, 30, 31] 259 | RETURN p 260 | LIMIT 1000 261 | """, 262 | "Non-default permissions on IssuancePolicy nodes": """ 263 | MATCH p = (s:Base)-[:GenericAll|GenericWrite|Owns|WriteOwner|WriteDacl]->(:IssuancePolicy) 264 | WHERE NOT s.objectid ENDS WITH '-512' AND NOT s.objectid ENDS WITH '-519' 265 | RETURN p 266 | LIMIT 1000 267 | """, 268 | "Enrollment rights on CertTemplates with OIDGroupLink": """ 269 | MATCH p = (:Base)-[:Enroll|GenericAll|AllExtendedRights]->(:CertTemplate)-[:ExtendedByPolicy]->(:IssuancePolicy)-[:OIDGroupLink]->(:Group) 270 | RETURN p 271 | LIMIT 1000 272 | """, 273 | "Enabled Tier Zero / High Value principals inactive for 60 days": """ 274 | WITH 60 as inactive_days 275 | MATCH (n:Base) 276 | WHERE COALESCE(n.system_tags, '') CONTAINS 'admin_tier_0' 277 | AND n.enabled = true 278 | AND n.lastlogontimestamp < (datetime().epochseconds - (inactive_days * 86400)) // Replicated value 279 | AND n.lastlogon < (datetime().epochseconds - (inactive_days * 86400)) // Non-replicated value 280 | AND n.whencreated < (datetime().epochseconds - (inactive_days * 86400)) // Exclude recently created principals 281 | AND NOT n.name STARTS WITH 'AZUREADKERBEROS.' // Removes false positive, Azure KRBTGT 282 | AND NOT n.objectid ENDS WITH '-500' // Removes false positive, built-in Administrator 283 | AND NOT n.name STARTS WITH 'AZUREADSSOACC.' // Removes false positive, Entra Seamless SSO 284 | RETURN n 285 | """, 286 | "Tier Zero / High Value enabled users not requiring smart card authentication": """ 287 | MATCH (u:User) 288 | WHERE COALESCE(u.system_tags, '') CONTAINS 'admin_tier_0' 289 | AND u.enabled = true 290 | AND u.smartcardrequired = false 291 | AND NOT u.name STARTS WITH 'MSOL_' // Removes false positive, Entra sync 292 | AND NOT u.name STARTS WITH 'PROVAGENTGMSA' // Removes false positive, Entra sync 293 | AND NOT u.name STARTS WITH 'ADSYNCMSA_' // Removes false positive, Entra sync 294 | RETURN u 295 | """, 296 | "Domains where any user can join a computer to the domain": """ 297 | MATCH (d:Domain) 298 | WHERE d.machineaccountquota > 0 299 | RETURN d 300 | """, 301 | "Domains with smart card accounts where smart account passwords do not expire": """ 302 | MATCH (s:Domain)-[:Contains*1..]->(t:Base) 303 | WHERE s.expirepasswordsonsmartcardonlyaccounts = false 304 | AND t.enabled = true 305 | AND t.smartcardrequired = true 306 | RETURN s 307 | """, 308 | "Two-way forest trusts enabled for delegation": """ 309 | MATCH p=(n:Domain)-[r:TrustedBy]->(m:Domain) 310 | WHERE (m)-[:TrustedBy]->(n) 311 | AND r.trusttype = 'Forest' 312 | AND r.tgtdelegationenabled = true 313 | RETURN p 314 | """, 315 | "Computers with unsupported operating systems": """ 316 | MATCH (c:Computer) 317 | WHERE c.operatingsystem =~ '(?i).*Windows.* (2000|2003|2008|2012|xp|vista|7|8|me|nt).*' 318 | RETURN c 319 | LIMIT 100 320 | """, 321 | "Users which do not require password to authenticate": """ 322 | MATCH (u:User) 323 | WHERE u.passwordnotreqd = true 324 | RETURN u 325 | LIMIT 100 326 | """, 327 | "Users with passwords not rotated in over 1 year": """ 328 | WITH 365 as days_since_change 329 | MATCH (u:User) 330 | WHERE u.pwdlastset < (datetime().epochseconds - (days_since_change * 86400)) 331 | AND NOT u.pwdlastset IN [-1.0, 0.0] 332 | RETURN u 333 | LIMIT 100 334 | """, 335 | "Nested groups within Tier Zero / High Value": """ 336 | MATCH p=(t:Group)<-[:MemberOf*..]-(s:Group) 337 | WHERE COALESCE(t.system_tags, '') CONTAINS 'admin_tier_0' 338 | AND NOT s.objectid ENDS WITH '-512' // Domain Admins 339 | AND NOT s.objectid ENDS WITH '-519' // Enterprise Admins 340 | RETURN p 341 | LIMIT 1000 342 | """, 343 | "Disabled Tier Zero / High Value principals": """ 344 | MATCH (n:Base) 345 | WHERE COALESCE(n.system_tags, '') CONTAINS 'admin_tier_0' 346 | AND n.enabled = false 347 | AND NOT n.objectid ENDS WITH '-502' // Removes false positive, KRBTGT 348 | AND NOT n.objectid ENDS WITH '-500' // Removes false positive, built-in Administrator 349 | RETURN n 350 | LIMIT 100 351 | """, 352 | "Principals with passwords stored using reversible encryption": """ 353 | MATCH (n:Base) 354 | WHERE n.encryptedtextpwdallowed = true 355 | RETURN n 356 | """, 357 | "Principals with DES-only Kerberos authentication": """ 358 | MATCH (n:Base) 359 | WHERE n.enabled = true 360 | AND n.usedeskeyonly = true 361 | RETURN n 362 | """, 363 | "Principals with weak supported Kerberos encryption types": """ 364 | MATCH (u:Base) 365 | WHERE 'DES-CBC-CRC' IN u.supportedencryptiontypes 366 | OR 'DES-CBC-MD5' IN u.supportedencryptiontypes 367 | OR 'RC4-HMAC-MD5' IN u.supportedencryptiontypes 368 | RETURN u 369 | """, 370 | "Tier Zero / High Value users with non-expiring passwords": """ 371 | MATCH (u:User) 372 | WHERE u.enabled = true 373 | AND u.pwdneverexpires = true 374 | and COALESCE(u.system_tags, '') CONTAINS 'admin_tier_0' 375 | RETURN u 376 | LIMIT 100 377 | """, 378 | "All coerce and NTLM relay edges": """ 379 | MATCH p = (n:Base)-[:CoerceAndRelayNTLMToLDAP|CoerceAndRelayNTLMToLDAPS|CoerceAndRelayNTLMToADCS|CoerceAndRelayNTLMToSMB]->(:Base) 380 | RETURN p LIMIT 500 381 | """, 382 | "ESC8-vulnerable Enterprise CAs": """ 383 | MATCH (n:EnterpriseCA) 384 | WHERE n.hasvulnerableendpoint=true 385 | RETURN n 386 | """, 387 | "Computers with the outgoing NTLM setting set to Deny all": """ 388 | MATCH (c:Computer) 389 | WHERE c.restrictoutboundntlm = True 390 | RETURN c LIMIT 1000 391 | """, 392 | "Computers with membership in Protected Users": """ 393 | MATCH p = (:Base)-[:MemberOf*1..]->(g:Group) 394 | WHERE g.objectid ENDS WITH "-525" 395 | RETURN p LIMIT 1000 396 | """, 397 | "DCs vulnerable to NTLM relay to LDAP attacks": """ 398 | MATCH p = (dc:Computer)-[:DCFor]->(:Domain) 399 | WHERE (dc.ldapavailable = True AND dc.ldapsigning = False) 400 | OR (dc.ldapsavailable = True AND dc.ldapsepa = False) 401 | OR (dc.ldapavailable = True AND dc.ldapsavailable = True AND dc.ldapsigning = False and dc.ldapsepa = True) 402 | RETURN p 403 | """, 404 | "Computers with the WebClient running": """ 405 | MATCH (c:Computer) 406 | WHERE c.webclientrunning = True 407 | RETURN c LIMIT 1000 408 | """, 409 | "Computers not requiring inbound SMB signing": """ 410 | MATCH (n:Computer) 411 | WHERE n.smbsigning = False 412 | RETURN n 413 | """ 414 | } 415 | 416 | 417 | class CypherPredefined(CommandBase): 418 | cmd = "cypher_predefined" 419 | needs_admin = False 420 | help_cmd = cmd 421 | description = "Run one of Bloodhound's predefined queries" 422 | version = 1 423 | author = "@its_a_feature_" 424 | argument_class = CypherPredefinedArguments 425 | browser_script = BrowserScript(script_name="cypher", author="@its_a_feature_", for_new_ui=True) 426 | attackmapping = [] 427 | 428 | async def create_go_tasking(self, 429 | taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse: 430 | response = MythicCommandBase.PTTaskCreateTaskingMessageResponse( 431 | TaskID=taskData.Task.ID, 432 | Success=False, 433 | Completed=True, 434 | DisplayParams=f"{taskData.args.get_arg('query_name')}" 435 | ) 436 | uri = f"/api/v2/graphs/cypher" 437 | body = json.dumps({ 438 | "include_properties": True, 439 | "query": predefined_queries[taskData.args.get_arg("query_name")] 440 | }).encode() 441 | response.Stdout = predefined_queries[taskData.args.get_arg("query_name")] 442 | try: 443 | response_code, response_data = await BloodhoundAPI.query_bloodhound(taskData, 444 | method='POST', 445 | body=body, 446 | uri=uri) 447 | return await BloodhoundAPI.process_standard_response(response_code=response_code, 448 | response_data=response_data, 449 | taskData=taskData, 450 | response=response) 451 | 452 | except Exception as e: 453 | await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage( 454 | TaskID=taskData.Task.ID, 455 | Response=f"{e}".encode("UTF8"), 456 | )) 457 | response.TaskStatus = "Error: Bloodhound Access Error" 458 | return response 459 | 460 | async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse: 461 | resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True) 462 | return resp 463 | --------------------------------------------------------------------------------