├── 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 | 
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 | 
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 |
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 |
28 |
--------------------------------------------------------------------------------
/Payload_Type/bloodhound/bloodhound/agent_functions/bloodhound.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
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 |
--------------------------------------------------------------------------------