From 341d7e81dd4e00a1416093db8ce7fc24804116e4 Mon Sep 17 00:00:00 2001 From: maxdcb <40819564+maxDcb@users.noreply.github.com> Date: Tue, 5 May 2026 18:51:09 +0200 Subject: [PATCH] Inject --- C2Client/C2Client/ConsolePanel.py | 18 ++- .../assistant_agent/tools/schemas/inject.json | 17 ++- .../assistant_agent/test_command_builder.py | 2 +- C2Client/tests/test_console_panel.py | 36 +++++ core | 2 +- packaging/validate_release.py | 1 + teamServer/CMakeLists.txt | 1 + teamServer/teamServer/TeamServer.cpp | 7 + .../TeamServerInjectCommandPreparer.cpp | 136 ++++++++++++++++++ .../TeamServerInjectCommandPreparer.hpp | 35 +++++ ...amServerCommandPreparationServiceTests.cpp | 85 +++++++++++ 11 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 teamServer/teamServer/TeamServerInjectCommandPreparer.cpp create mode 100644 teamServer/teamServer/TeamServerInjectCommandPreparer.hpp diff --git a/C2Client/C2Client/ConsolePanel.py b/C2Client/C2Client/ConsolePanel.py index de261d7..4fa5ef2 100644 --- a/C2Client/C2Client/ConsolePanel.py +++ b/C2Client/C2Client/ConsolePanel.py @@ -180,8 +180,13 @@ def _argument_artifact_completion_values(artifact: Any) -> list[str]: ]) -def _artifact_value_continuations(arg: Any) -> list[str]: +def _artifact_value_continuations(arg: Any, command_name: str = "") -> list[str]: name = _arg_name(arg) + if command_name == "inject": + if name == "--donut-dll": + return ["--pid", "--method"] + if name in {"--raw", "--donut-exe"}: + return ["--pid"] if name == "--donut-exe": return ["--"] if name == "--donut-dll": @@ -194,8 +199,9 @@ def _add_artifact_completions( grpcClient: Any, arg: Any, session: Any | None, + command_name: str = "", ) -> None: - continuations = _artifact_value_continuations(arg) + continuations = _artifact_value_continuations(arg, command_name) for artifact in _load_artifacts_for_arg(grpcClient, arg, session): for value in _argument_artifact_completion_values(artifact): _add_completion_value(children, value) @@ -211,6 +217,7 @@ def _build_flag_entries( session: Any | None = None, *, include_context_only: bool = False, + command_name: str = "", ) -> list[tuple[str, list]]: entries: list[tuple[str, list]] = [] for arg in args: @@ -226,7 +233,7 @@ def _build_flag_entries( continue for value in getattr(arg, "values", []): _add_completion_value(flag_entry[1], value) - _add_artifact_completions(flag_entry[1], grpcClient, arg, session) + _add_artifact_completions(flag_entry[1], grpcClient, arg, session, command_name) return entries @@ -254,7 +261,8 @@ def _add_arg_completions( session: Any | None = None, ) -> None: args = list(getattr(command, "args", [])) - flag_entries = _build_flag_entries(args, grpcClient, session) + command_name = str(getattr(command, "name", "") or "") + flag_entries = _build_flag_entries(args, grpcClient, session, command_name=command_name) _merge_completion_entries(children, flag_entries) _add_mode_value_flag_completions(children, args, grpcClient, session) @@ -267,7 +275,7 @@ def _add_arg_completions( continue for value in getattr(arg, "values", []): _add_completion_value(children, value) - _add_artifact_completions(children, grpcClient, arg, session) + _add_artifact_completions(children, grpcClient, arg, session, command_name) first_positional_done = True diff --git a/C2Client/C2Client/assistant_agent/tools/schemas/inject.json b/C2Client/C2Client/assistant_agent/tools/schemas/inject.json index 298afef..d565853 100644 --- a/C2Client/C2Client/assistant_agent/tools/schemas/inject.json +++ b/C2Client/C2Client/assistant_agent/tools/schemas/inject.json @@ -1,7 +1,7 @@ { "name": "inject", - "description": "Inject raw shellcode or Donut-generated payload into a process.", - "command_template": "inject {payload_type} {input_file:q} {pid} {method:q?} {arguments:raw?}", + "description": "Inject raw shellcode or TeamServer-generated shellcode into a process.", + "command_template": "inject {payload_type} {input_file:q} --pid {pid} [--method {method:q}] {arguments:raw?}", "parameters": { "type": "object", "properties": { @@ -15,11 +15,11 @@ }, "payload_type": { "type": "string", - "description": "Payload source type accepted by init(): -r raw shellcode, -e .NET executable, or -d .NET DLL.", + "description": "Payload source flag: --raw for shellcode, --donut-exe for an executable, or --donut-dll for a DLL.", "enum": [ - "-r", - "-e", - "-d" + "--raw", + "--donut-exe", + "--donut-dll" ] }, "input_file": { @@ -28,12 +28,11 @@ }, "pid": { "type": "integer", - "description": "Target process id.", - "minimum": 0 + "description": "Target process id. Use a negative value to spawn the configured process before injection." }, "method": { "type": "string", - "description": "DLL method name. Required only when payload_type is -d.", + "description": "DLL method name. Required only when payload_type is --donut-dll.", "default": "" }, "arguments": { diff --git a/C2Client/tests/assistant_agent/test_command_builder.py b/C2Client/tests/assistant_agent/test_command_builder.py index 7a6a889..46e6e7c 100644 --- a/C2Client/tests/assistant_agent/test_command_builder.py +++ b/C2Client/tests/assistant_agent/test_command_builder.py @@ -72,7 +72,7 @@ def test_build_command_line_rejects_missing_required_argument(): ("enumerateShares", {"host": "fileserver"}, "enumerateShares fileserver"), ("evasion", {"action": "ReadMemory", "address": "0x1234", "value": "16"}, "evasion ReadMemory 0x1234 16"), ("getEnv", {}, "getEnv"), - ("inject", {"payload_type": "-d", "input_file": "payload.dll", "pid": 4242, "method": "Run", "arguments": "a b"}, "inject -d payload.dll 4242 Run a b"), + ("inject", {"payload_type": "--donut-dll", "input_file": "payload.dll", "pid": 4242, "method": "Run", "arguments": "a b"}, "inject --donut-dll payload.dll --pid 4242 --method Run a b"), ("ipConfig", {}, "ipConfig"), ("kerberosUseTicket", {"ticket_file": "/tmp/ticket.kirbi"}, "kerberosUseTicket /tmp/ticket.kirbi"), ("keyLogger", {"action": "start"}, "keyLogger start"), diff --git a/C2Client/tests/test_console_panel.py b/C2Client/tests/test_console_panel.py index 4f648df..9da7a2f 100644 --- a/C2Client/tests/test_console_panel.py +++ b/C2Client/tests/test_console_panel.py @@ -347,6 +347,8 @@ def test_command_specs_add_flag_completions_without_positional_mode_mix(): ]) if query.name_contains == ".dll": return iter([SimpleNamespace(name="Tools/Example.dll", display_name="Example.dll")]) + if query.name_contains == ".bin": + return iter([SimpleNamespace(name="payloads/loader.bin", display_name="loader.bin")]) return iter([]) artifact_filter_exe = SimpleNamespace( @@ -367,6 +369,15 @@ def test_command_specs_add_flag_completions_without_positional_mode_mix(): runtime="any", name_contains=".dll", ) + artifact_filter_bin = SimpleNamespace( + category="tool", + scope="server", + target="teamserver", + platform="windows", + arch="", + runtime="any", + name_contains=".bin", + ) assembly_spec = SimpleNamespace( name="assemblyExec", kind="module", @@ -414,6 +425,31 @@ def test_command_specs_add_flag_completions_without_positional_mode_mix(): assert grpc.queries[0].runtime == "any" assert grpc.queries[0].name_contains == ".exe" + inject_spec = SimpleNamespace( + name="inject", + kind="module", + examples=[ + "inject --raw loader.bin --pid 4321", + "inject --donut-exe Seatbelt.exe --pid 4321 -- arg", + "inject --donut-dll Tool.dll --pid -1 --method EntryPoint -- arg", + ], + args=[ + SimpleNamespace(name="--pid", type="flag", values=[]), + SimpleNamespace(name="--raw", type="flag", values=[], artifact_filter=artifact_filter_bin), + SimpleNamespace(name="--donut-exe", type="flag", values=[], artifact_filter=artifact_filter_exe), + SimpleNamespace(name="--donut-dll", type="flag", values=[], artifact_filter=artifact_filter_dll), + SimpleNamespace(name="--method", type="flag", values=[]), + ], + ) + + server_data = command_specs_to_completer_data([inject_spec], grpcClient=grpc) + inject_children = _completion_children(server_data, "inject") + raw_children = _completion_children(inject_children, "--raw") + assert ("--pid", []) in _completion_children(raw_children, "payloads/loader.bin") + inject_dll_children = _completion_children(inject_children, "--donut-dll") + assert ("--pid", []) in _completion_children(inject_dll_children, "Tools/Example.dll") + assert ("--method", []) in _completion_children(inject_dll_children, "Tools/Example.dll") + def test_contextual_completer_uses_artifacts_listeners_and_module_specs(): class FakeGrpc: diff --git a/core b/core index 9b72807..80a92ee 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 9b72807514b9c7836bfb43af4745fccb3ffca395 +Subproject commit 80a92ee7eba04dad069fcdad91a47b4a93cf4be1 diff --git a/packaging/validate_release.py b/packaging/validate_release.py index 55de273..5708cd6 100644 --- a/packaging/validate_release.py +++ b/packaging/validate_release.py @@ -259,6 +259,7 @@ def validate_base_release(release_root: Path) -> None: _require_non_empty_file(command_specs_root / "modules" / "netstat.json") _require_non_empty_file(command_specs_root / "modules" / "shell.json") _require_non_empty_file(command_specs_root / "modules" / "assemblyExec.json") + _require_non_empty_file(command_specs_root / "modules" / "inject.json") _require_non_empty_file(client_root / "README.md") _require_non_empty_file(client_root / "pyproject.toml") diff --git a/teamServer/CMakeLists.txt b/teamServer/CMakeLists.txt index ebbcf1c..d0f147b 100644 --- a/teamServer/CMakeLists.txt +++ b/teamServer/CMakeLists.txt @@ -12,6 +12,7 @@ set(TEAMSERVER_CORE_SOURCES teamServer/TeamServerCommandPreparationService.cpp teamServer/TeamServerGeneratedArtifactStore.cpp teamServer/TeamServerHelpService.cpp + teamServer/TeamServerInjectCommandPreparer.cpp teamServer/TeamServerListenerArtifactService.cpp teamServer/TeamServerModuleLoader.cpp teamServer/TeamServerShellcodeService.cpp diff --git a/teamServer/teamServer/TeamServer.cpp b/teamServer/teamServer/TeamServer.cpp index 545073b..5ce713a 100644 --- a/teamServer/teamServer/TeamServer.cpp +++ b/teamServer/teamServer/TeamServer.cpp @@ -10,6 +10,7 @@ #include "TeamServerCommandPreparationService.hpp" #include "TeamServerGeneratedArtifactStore.hpp" #include "TeamServerHelpService.hpp" +#include "TeamServerInjectCommandPreparer.hpp" #include "TeamServerListenerArtifactService.hpp" #include "TeamServerListenerSessionService.hpp" #include "TeamServerModuleLoader.hpp" @@ -97,6 +98,12 @@ TeamServer::TeamServer(const nlohmann::json& config) m_shellcodeService, m_generatedArtifactStore, m_moduleCmd)); + commandPreparers.push_back(std::make_unique( + m_logger, + runtimeConfig, + m_shellcodeService, + m_generatedArtifactStore, + m_moduleCmd)); m_commandPreparationService = std::make_unique( m_logger, runtimeConfig, diff --git a/teamServer/teamServer/TeamServerInjectCommandPreparer.cpp b/teamServer/teamServer/TeamServerInjectCommandPreparer.cpp new file mode 100644 index 0000000..10a8822 --- /dev/null +++ b/teamServer/teamServer/TeamServerInjectCommandPreparer.cpp @@ -0,0 +1,136 @@ +#include "TeamServerInjectCommandPreparer.hpp" + +#include +#include + +#include "modules/Inject/InjectCommandOptions.hpp" + +namespace fs = std::filesystem; + +namespace +{ +std::string resolveSourcePath( + const TeamServerRuntimeConfig& runtimeConfig, + const std::string& path, + const std::string& windowsArch) +{ + if (path.empty()) + return ""; + if (fs::exists(path)) + return path; + + fs::path toolPath = fs::path(runtimeConfig.toolsDirectoryPath) / path; + if (fs::exists(toolPath)) + return toolPath.string(); + + fs::path beaconPath = fs::path(runtimeConfig.windowsBeaconsDirectoryPath) / path; + if (fs::exists(beaconPath)) + return beaconPath.string(); + + fs::path archBeaconPath = fs::path(runtimeConfig.windowsBeaconsDirectoryPath) / windowsArch / path; + if (fs::exists(archBeaconPath)) + return archBeaconPath.string(); + + return path; +} + +ModuleCmd* findModule(std::vector>& modules, const std::string& name) +{ + const std::string loweredName = inject_command::lowerCopy(name); + for (const auto& module : modules) + { + if (module && inject_command::lowerCopy(module->getName()) == loweredName) + return module.get(); + } + return nullptr; +} +} // namespace + +TeamServerInjectCommandPreparer::TeamServerInjectCommandPreparer( + std::shared_ptr logger, + TeamServerRuntimeConfig runtimeConfig, + std::shared_ptr shellcodeService, + std::shared_ptr artifactStore, + std::vector>& moduleCmd) + : m_logger(std::move(logger)), + m_runtimeConfig(std::move(runtimeConfig)), + m_shellcodeService(std::move(shellcodeService)), + m_artifactStore(std::move(artifactStore)), + m_moduleCmd(moduleCmd) +{ +} + +bool TeamServerInjectCommandPreparer::canPrepare(const std::string& instruction) const +{ + return inject_command::lowerCopy(instruction) == "inject"; +} + +TeamServerCommandPreparerResult TeamServerInjectCommandPreparer::prepare( + const TeamServerCommandPreparerContext& context, + C2Message& c2Message) const +{ + TeamServerCommandPreparerResult result; + result.handled = true; + result.status = -1; + + inject_command::CommandOptions options = inject_command::parseCommandOptions(context.tokens); + if (!options.error.empty()) + { + c2Message.set_returnvalue(options.error + "\n"); + return result; + } + + if (!m_shellcodeService || !m_artifactStore) + { + c2Message.set_returnvalue("Shellcode preparation service is not available.\n"); + return result; + } + + TeamServerShellcodeRequest shellcodeRequest; + shellcodeRequest.generator = options.generator; + shellcodeRequest.sourcePath = resolveSourcePath(m_runtimeConfig, options.sourcePath, context.windowsArch); + shellcodeRequest.sourceType = options.sourceType; + shellcodeRequest.arch = context.windowsArch; + shellcodeRequest.method = options.method; + shellcodeRequest.arguments = options.arguments; + shellcodeRequest.exitPolicy = "process"; + + TeamServerShellcodeResult shellcode = m_shellcodeService->generate(shellcodeRequest); + if (!shellcode.ok) + { + c2Message.set_returnvalue(shellcode.message + "\n"); + return result; + } + + TeamServerGeneratedArtifactRequest artifactRequest; + artifactRequest.nameHint = "inject-" + fs::path(shellcodeRequest.sourcePath).filename().string() + ".bin"; + artifactRequest.bytes = shellcode.bytes; + artifactRequest.platform = context.isWindows ? "windows" : "linux"; + artifactRequest.arch = context.isWindows ? context.windowsArch : "any"; + artifactRequest.source = shellcode.generator; + artifactRequest.description = "Generated shellcode for inject."; + artifactRequest.tags = {"inject", shellcode.sourceType}; + TeamServerGeneratedArtifactRecord artifact = m_artifactStore->store(artifactRequest); + if (artifact.path.empty()) + { + c2Message.set_returnvalue("Could not store generated shellcode artifact.\n"); + return result; + } + + ModuleCmd* module = findModule(m_moduleCmd, "inject"); + if (!module) + { + c2Message.set_returnvalue("Module inject not found.\n"); + return result; + } + + ModulePreparedShellcodeTask task; + task.inputFile = artifact.path; + task.payload = shellcode.bytes; + task.pid = options.pid; + task.displayCommand = options.displayCommand; + result.status = module->initPreparedShellcode(task, c2Message); + if (result.status == 0 && m_logger) + m_logger->info("inject prepared shellcode artifact {}", artifact.path); + return result; +} diff --git a/teamServer/teamServer/TeamServerInjectCommandPreparer.hpp b/teamServer/teamServer/TeamServerInjectCommandPreparer.hpp new file mode 100644 index 0000000..c3704fa --- /dev/null +++ b/teamServer/teamServer/TeamServerInjectCommandPreparer.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +#include "TeamServerCommandPreparer.hpp" +#include "TeamServerGeneratedArtifactStore.hpp" +#include "TeamServerRuntimeConfig.hpp" +#include "TeamServerShellcodeService.hpp" +#include "modules/ModuleCmd/ModuleCmd.hpp" +#include "spdlog/logger.h" + +class TeamServerInjectCommandPreparer final : public TeamServerCommandPreparer +{ +public: + TeamServerInjectCommandPreparer( + std::shared_ptr logger, + TeamServerRuntimeConfig runtimeConfig, + std::shared_ptr shellcodeService, + std::shared_ptr artifactStore, + std::vector>& moduleCmd); + + bool canPrepare(const std::string& instruction) const override; + TeamServerCommandPreparerResult prepare( + const TeamServerCommandPreparerContext& context, + C2Message& c2Message) const override; + +private: + std::shared_ptr m_logger; + TeamServerRuntimeConfig m_runtimeConfig; + std::shared_ptr m_shellcodeService; + std::shared_ptr m_artifactStore; + std::vector>& m_moduleCmd; +}; diff --git a/teamServer/tests/TeamServerCommandPreparationServiceTests.cpp b/teamServer/tests/TeamServerCommandPreparationServiceTests.cpp index 715800f..266c7e6 100644 --- a/teamServer/tests/TeamServerCommandPreparationServiceTests.cpp +++ b/teamServer/tests/TeamServerCommandPreparationServiceTests.cpp @@ -9,6 +9,7 @@ #include "TeamServerArtifactCatalog.hpp" #include "TeamServerCommandPreparationService.hpp" #include "TeamServerGeneratedArtifactStore.hpp" +#include "TeamServerInjectCommandPreparer.hpp" #include "TeamServerShellcodeService.hpp" namespace fs = std::filesystem; @@ -97,6 +98,7 @@ public: c2Message.set_instruction(getName()); c2Message.set_cmd(task.displayCommand); c2Message.set_args(task.executionMode); + c2Message.set_pid(task.pid); c2Message.set_inputfile(task.inputFile); c2Message.set_data(task.payload); return 0; @@ -318,6 +320,87 @@ void testPrepareAssemblyExecDonutReportsMissingSource() assert(service.prepareMessage("assemblyExec --mode thread --donut-exe missing.exe", message, true, "x64") == -1); assert(message.returnvalue().find("Couldn't open Donut source file.") != std::string::npos); } + +void testPrepareInjectUsesShellcodeServiceAndGeneratedArtifactStore() +{ + ScopedPath tempRoot(makeTempDirectory("inject-preparer")); + TeamServerRuntimeConfig runtimeConfig = makeRuntimeConfig(tempRoot.path()); + writeFile(fs::path(runtimeConfig.toolsDirectoryPath) / "payload.bin", "INJECT-SHELLCODE"); + + CommonCommands commonCommands; + std::vector> modules; + modules.push_back(std::make_unique("inject")); + + auto shellcodeService = std::make_shared(makeLogger()); + auto artifactStore = std::make_shared(runtimeConfig); + std::vector> preparers; + preparers.push_back(std::make_unique( + makeLogger(), + runtimeConfig, + shellcodeService, + artifactStore, + modules)); + + TeamServerCommandPreparationService service( + makeLogger(), + runtimeConfig, + commonCommands, + modules, + std::move(preparers)); + + C2Message message; + assert(service.prepareMessage("inject --raw payload.bin --pid 4321", message, true, "amd64") == 0); + assert(message.instruction() == "inject"); + assert(message.pid() == 4321); + assert(message.data() == "INJECT-SHELLCODE"); + assert(message.cmd() == "--raw payload.bin --pid 4321"); + assert(message.inputfile().find("GeneratedArtifacts") != std::string::npos); + assert(fs::exists(message.inputfile())); + assert(fs::exists(message.inputfile() + ".artifact.json")); + + TeamServerArtifactCatalog catalog(runtimeConfig); + TeamServerArtifactQuery query; + query.category = "payload"; + query.scope = "generated"; + query.runtime = "shellcode"; + const std::vector artifacts = catalog.listArtifacts(query); + assert(artifacts.size() == 1); + assert(artifacts[0].source == "raw"); + assert(artifacts[0].platform == "windows"); + assert(artifacts[0].arch == "x64"); + assert(artifacts[0].description == "Generated shellcode for inject."); +} + +void testPrepareInjectDonutReportsMissingSource() +{ + ScopedPath tempRoot(makeTempDirectory("inject-donut-missing")); + TeamServerRuntimeConfig runtimeConfig = makeRuntimeConfig(tempRoot.path()); + + CommonCommands commonCommands; + std::vector> modules; + modules.push_back(std::make_unique("inject")); + + auto shellcodeService = std::make_shared(makeLogger()); + auto artifactStore = std::make_shared(runtimeConfig); + std::vector> preparers; + preparers.push_back(std::make_unique( + makeLogger(), + runtimeConfig, + shellcodeService, + artifactStore, + modules)); + + TeamServerCommandPreparationService service( + makeLogger(), + runtimeConfig, + commonCommands, + modules, + std::move(preparers)); + + C2Message message; + assert(service.prepareMessage("inject --donut-exe missing.exe --pid 4321 -- arg1", message, true, "x64") == -1); + assert(message.returnvalue().find("Couldn't open Donut source file.") != std::string::npos); +} } // namespace int main() @@ -328,5 +411,7 @@ int main() testPrepareLoadModuleUsesWindowsSessionArchitecture(); testPrepareAssemblyExecUsesShellcodeServiceAndGeneratedArtifactStore(); testPrepareAssemblyExecDonutReportsMissingSource(); + testPrepareInjectUsesShellcodeServiceAndGeneratedArtifactStore(); + testPrepareInjectDonutReportsMissingSource(); return 0; }