Add SSH exec module using libssh2 (#26)

* Add SSH execution module

* SshExec ok
This commit is contained in:
Maxime dcb
2025-11-01 11:47:44 -07:00
committed by GitHub
parent 3cb1f97257
commit 3693fbee06
7 changed files with 740 additions and 5 deletions
+12 -2
View File
@@ -26,6 +26,7 @@ endif()
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
include(FetchContent)
@@ -58,7 +59,7 @@ if(WIN32)
FetchContent_Declare(
MemoryModule
GIT_REPOSITORY https://github.com/maxDcb/MemoryModule.git
GIT_TAG master
GIT_TAG develop
)
else()
FetchContent_Declare(
@@ -166,6 +167,15 @@ FetchContent_Declare(
)
FetchContent_MakeAvailable(httplib)
set(LIBSSH2_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(LIBSSH2_BUILD_TESTS OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
libssh2
GIT_REPOSITORY https://github.com/libssh2/libssh2.git
GIT_TAG libssh2-1.11.1
)
FetchContent_MakeAvailable(libssh2)
find_package(OpenSSL REQUIRED)
add_library(openssl::openssl INTERFACE IMPORTED)
target_link_libraries(openssl::openssl INTERFACE OpenSSL::SSL OpenSSL::Crypto)
@@ -189,7 +199,7 @@ if(C2CORE_BUILD_TESTS)
FetchContent_Declare(
nlohmann_json
GIT_REPOSITORY https://github.com/nlohmann/json.git
GIT_TAG v3.11.2
GIT_TAG v3.12.0
)
FetchContent_MakeAvailable(Catch2 nlohmann_json)
include_directories(${nlohmann_json_SOURCE_DIR}/include)
+17
View File
@@ -0,0 +1,17 @@
###########################################################################
# libssh2 installation details
###########################################################################
prefix=@prefix@
exec_prefix=@exec_prefix@
libdir=@libdir@
includedir=@includedir@
Name: libssh2
URL: https://www.libssh2.org/
Description: Library for SSH-based communication
Version: @LIBSSH2VER@
Requires.private: @LIBSREQUIRED@
Libs: -L${libdir} -lssh2 @LIBS@
Libs.private: @LIBS@
Cflags: -I${includedir}
+2 -3
View File
@@ -34,6 +34,7 @@ add_subdirectory(MiniDump)
add_subdirectory(DotnetExec)
add_subdirectory(PwSh)
add_subdirectory(Shell)
add_subdirectory(Registry)
## Enum
add_subdirectory(ChangeDirectory)
@@ -57,6 +58,4 @@ add_subdirectory(TaskScheduler)
add_subdirectory(WinRM)
add_subdirectory(DcomExec)
add_subdirectory(CimExec)
## Registry
add_subdirectory(Registry)
add_subdirectory(SshExec)
+20
View File
@@ -0,0 +1,20 @@
include_directories(../)
add_library(SshExec SHARED SshExec.cpp)
set_property(TARGET SshExec PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded")
target_link_libraries(SshExec PRIVATE libssh2_static)
if (WIN32)
target_link_libraries(SshExec PRIVATE ws2_32)
endif()
add_custom_command(TARGET SshExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:SshExec> "${CMAKE_SOURCE_DIR}/Release/Modules/$<TARGET_FILE_NAME:SshExec>")
if(C2CORE_BUILD_TESTS)
add_executable(testsSshExec tests/testsSshExec.cpp SshExec.cpp)
target_link_libraries(testsSshExec PRIVATE libssh2_static)
if (WIN32)
target_link_libraries(testsSshExec PRIVATE ws2_32)
endif()
add_custom_command(TARGET testsSshExec POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:testsSshExec> "${CMAKE_SOURCE_DIR}/Tests/$<TARGET_FILE_NAME:testsSshExec>")
add_test(NAME testsSshExec COMMAND "${CMAKE_SOURCE_DIR}/Tests/$<TARGET_FILE_NAME:testsSshExec>")
endif()
+592
View File
@@ -0,0 +1,592 @@
#include "SshExec.hpp"
#include "Common.hpp"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstring>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include <libssh2.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#else
#include <netdb.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
using namespace std;
constexpr std::string_view moduleNameSshExec = "sshExec";
constexpr unsigned long long moduleHashSshExec = djb2(moduleNameSshExec);
#ifdef _WIN32
extern "C" __declspec(dllexport) SshExec* SshExecConstructor()
{
return new SshExec();
}
#else
extern "C" __attribute__((visibility("default"))) SshExec* SshExecConstructor()
{
return new SshExec();
}
#endif
namespace
{
#ifdef _WIN32
using SocketHandle = SOCKET;
constexpr SocketHandle InvalidSocket = INVALID_SOCKET;
std::string formatWindowsError(int errorCode)
{
LPVOID lpMsgBuf = nullptr;
DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS;
DWORD lang = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT);
DWORD size = FormatMessageA(flags, nullptr, static_cast<DWORD>(errorCode), lang, reinterpret_cast<LPSTR>(&lpMsgBuf), 0, nullptr);
std::string message;
if (size && lpMsgBuf)
{
message.assign(static_cast<LPSTR>(lpMsgBuf), size);
LocalFree(lpMsgBuf);
}
else
{
message = "error code " + std::to_string(errorCode);
}
return message;
}
std::string lastSocketError()
{
return formatWindowsError(WSAGetLastError());
}
void closeSocket(SocketHandle socket)
{
if (socket != InvalidSocket)
{
closesocket(socket);
}
}
class WsaInitializer
{
public:
WsaInitializer()
{
int rc = WSAStartup(MAKEWORD(2, 2), &m_data);
m_success = (rc == 0);
if (!m_success)
{
m_error = formatWindowsError(rc);
}
}
~WsaInitializer()
{
if (m_success)
{
WSACleanup();
}
}
bool ok() const
{
return m_success;
}
const std::string& error() const
{
return m_error;
}
private:
WSADATA m_data{};
bool m_success = false;
std::string m_error;
};
#else
using SocketHandle = int;
constexpr SocketHandle InvalidSocket = -1;
std::string lastSocketError()
{
return std::strerror(errno);
}
void closeSocket(SocketHandle socket)
{
if (socket != InvalidSocket)
{
::close(socket);
}
}
#endif
class Libssh2Initializer
{
public:
Libssh2Initializer()
{
m_result = libssh2_init(0);
}
~Libssh2Initializer()
{
if (m_result == 0)
{
libssh2_exit();
}
}
bool ok() const
{
return m_result == 0;
}
int errorCode() const
{
return m_result;
}
private:
int m_result;
};
std::string makeHex(const unsigned char* data, size_t length)
{
if (!data || length == 0)
{
return {};
}
std::ostringstream oss;
oss << std::hex << std::setfill('0');
for (size_t i = 0; i < length; ++i)
{
oss << std::setw(2) << static_cast<unsigned int>(data[i]);
}
return oss.str();
}
std::string lastLibssh2Error(LIBSSH2_SESSION* session, int code)
{
if (!session)
{
return "libssh2 error code " + std::to_string(code);
}
char* msg = nullptr;
int len = 0;
libssh2_session_last_error(session, &msg, &len, 0);
if (msg && len > 0)
{
return std::string(msg, len);
}
return "libssh2 error code " + std::to_string(code);
}
}
SshExec::SshExec()
#ifdef BUILD_TEAMSERVER
: ModuleCmd(std::string(moduleNameSshExec), moduleHashSshExec)
#else
: ModuleCmd("", moduleHashSshExec)
#endif
{
}
SshExec::~SshExec() = default;
std::string SshExec::getInfo()
{
std::ostringstream oss;
#ifdef BUILD_TEAMSERVER
oss << "SSH Exec Module:\n";
oss << "Execute a command on a remote SSH server using username/password authentication.\n";
oss << "Works against both Linux and Windows SSH servers.\n";
oss << "Options:\n";
oss << " -h, --host <host> Target hostname or IP.\n";
oss << " -P, --port <port> SSH port (default 22).\n";
oss << " -u, --user <user> Username.\n";
oss << " -p, --password <pass> Password.\n";
oss << " -c, --command <cmd> Command to execute.\n";
oss << " -- Treat the rest of the line as the command.\n";
oss << "Example:\n";
oss << " sshExec -h 10.0.0.5 -u admin -p Passw0rd! -c \"ipconfig /all\"\n";
oss << " sshExec -h server -- user pass -- \"/bin/echo hello\"\n";
#endif
return oss.str();
}
std::string SshExec::packParameters(const Parameters& params) const
{
std::string packed;
auto append = [&packed](const std::string& value)
{
packed.append(value);
packed.push_back('\0');
};
append(params.host);
append(params.port);
append(params.username);
append(params.password);
append(params.command);
return packed;
}
SshExec::Parameters SshExec::unpackParameters(const std::string& data) const
{
Parameters params;
std::vector<std::string> parts;
size_t start = 0;
while (start < data.size())
{
size_t end = data.find('\0', start);
if (end == std::string::npos)
{
break;
}
parts.emplace_back(data.substr(start, end - start));
start = end + 1;
}
if (parts.size() >= 5)
{
params.host = parts[0];
params.port = parts[1];
params.username = parts[2];
params.password = parts[3];
params.command = parts[4];
}
return params;
}
int SshExec::init(std::vector<std::string>& splitedCmd, C2Message& c2Message)
{
#if defined(BUILD_TEAMSERVER) || defined(BUILD_TESTS) || defined(C2CORE_BUILD_TESTS)
std::vector<std::string> args = regroupStrings(splitedCmd);
if (args.size() < 2)
{
c2Message.set_returnvalue(getInfo());
return -1;
}
Parameters params;
bool collectCommandTail = false;
for (size_t i = 1; i < args.size(); ++i)
{
const std::string& token = args[i];
if (collectCommandTail)
{
if (!params.command.empty())
{
params.command += " ";
}
params.command += token;
continue;
}
if (token == "--")
{
collectCommandTail = true;
continue;
}
if ((token == "-h" || token == "--host") && i + 1 < args.size())
{
params.host = args[++i];
}
else if ((token == "-P" || token == "--port") && i + 1 < args.size())
{
params.port = args[++i];
}
else if ((token == "-u" || token == "--user") && i + 1 < args.size())
{
params.username = args[++i];
}
else if ((token == "-p" || token == "--password") && i + 1 < args.size())
{
params.password = args[++i];
}
else if ((token == "-c" || token == "--command") && i + 1 < args.size())
{
params.command = args[++i];
}
else if (!token.empty() && token.front() != '-')
{
if (params.host.empty())
{
params.host = token;
}
else if (params.username.empty())
{
params.username = token;
}
else if (params.password.empty())
{
params.password = token;
}
else
{
if (!params.command.empty())
{
params.command += " ";
}
params.command += token;
}
}
}
if (params.host.empty() || params.username.empty() || params.password.empty() || params.command.empty())
{
c2Message.set_returnvalue("Missing required parameters.\n" + getInfo());
return -1;
}
c2Message.set_instruction(splitedCmd[0]);
c2Message.set_cmd(packParameters(params));
#endif
return 0;
}
int SshExec::process(C2Message& c2Message, C2Message& c2RetMessage)
{
c2RetMessage.set_instruction(c2Message.instruction());
c2RetMessage.set_cmd(c2Message.cmd());
Parameters params = unpackParameters(c2Message.cmd());
std::string result;
int error = executeSshCommand(params, result);
if (error != 0)
{
c2RetMessage.set_errorCode(error);
}
c2RetMessage.set_returnvalue(result);
return 0;
}
int SshExec::errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg)
{
#if defined(BUILD_TEAMSERVER) || defined(BUILD_TESTS) || defined(C2CORE_BUILD_TESTS)
int errorCode = c2RetMessage.errorCode();
if (errorCode > 0)
{
errorMsg = c2RetMessage.returnvalue();
}
#endif
return 0;
}
int SshExec::executeSshCommand(const Parameters& params, std::string& result) const
{
result.clear();
if (params.host.empty() || params.username.empty() || params.password.empty() || params.command.empty())
{
result = "Invalid parameters.";
return ErrorExecute;
}
#ifdef _WIN32
WsaInitializer wsa;
if (!wsa.ok())
{
result = "WSAStartup failed: " + wsa.error();
return ErrorSocketInit;
}
#endif
Libssh2Initializer sshInit;
if (!sshInit.ok())
{
result = "Failed to initialize libssh2 (" + std::to_string(sshInit.errorCode()) + ").";
return ErrorLibssh2Init;
}
addrinfo hints{};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
addrinfo* rawInfo = nullptr;
int addrRc = getaddrinfo(params.host.c_str(), params.port.c_str(), &hints, &rawInfo);
if (addrRc != 0)
{
#ifdef _WIN32
result = "getaddrinfo failed: " + std::string(gai_strerrorA(addrRc));
#else
result = "getaddrinfo failed: " + std::string(gai_strerror(addrRc));
#endif
return ErrorResolve;
}
std::unique_ptr<addrinfo, decltype(&freeaddrinfo)> addrList(rawInfo, freeaddrinfo);
SocketHandle socket = InvalidSocket;
for (addrinfo* ptr = addrList.get(); ptr != nullptr; ptr = ptr->ai_next)
{
SocketHandle candidate = static_cast<SocketHandle>(::socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol));
if (candidate == InvalidSocket)
{
continue;
}
if (::connect(candidate, ptr->ai_addr, static_cast<int>(ptr->ai_addrlen)) == 0)
{
socket = candidate;
break;
}
closeSocket(candidate);
}
if (socket == InvalidSocket)
{
result = "Unable to connect to " + params.host + ":" + params.port + " (" + lastSocketError() + ").";
return ErrorConnect;
}
LIBSSH2_SESSION* session = libssh2_session_init();
if (!session)
{
closeSocket(socket);
result = "Failed to create SSH session.";
return ErrorSessionInit;
}
bool handshakeDone = false;
libssh2_session_set_blocking(session, 1);
int rc = libssh2_session_handshake(session, socket);
if (rc != 0)
{
std::string message = lastLibssh2Error(session, rc);
libssh2_session_free(session);
closeSocket(socket);
result = "SSH handshake failed: " + message;
return ErrorHandshake;
}
handshakeDone = true;
rc = libssh2_userauth_password(session, params.username.c_str(), params.password.c_str());
if (rc != 0)
{
std::string message = lastLibssh2Error(session, rc);
if (handshakeDone)
{
libssh2_session_disconnect(session, "Authentication failed");
}
libssh2_session_free(session);
closeSocket(socket);
result = "Authentication failed: " + message;
return ErrorAuthentication;
}
LIBSSH2_CHANNEL* channel = libssh2_channel_open_session(session);
if (!channel)
{
libssh2_session_disconnect(session, "Failed to open channel");
libssh2_session_free(session);
closeSocket(socket);
result = "Failed to open SSH channel.";
return ErrorChannelOpen;
}
rc = libssh2_channel_exec(channel, params.command.c_str());
if (rc != 0)
{
std::string message = lastLibssh2Error(session, rc);
libssh2_channel_close(channel);
libssh2_channel_free(channel);
libssh2_session_disconnect(session, "Command execution failed");
libssh2_session_free(session);
closeSocket(socket);
result = "Failed to execute command: " + message;
return ErrorExecute;
}
std::string stdoutData;
std::string stderrData;
std::array<char, 4096> buffer{};
while (true)
{
ssize_t bytes = libssh2_channel_read(channel, buffer.data(), buffer.size());
if (bytes > 0)
{
stdoutData.append(buffer.data(), static_cast<size_t>(bytes));
}
else if (bytes == LIBSSH2_ERROR_EAGAIN)
{
continue;
}
else
{
break;
}
}
while (true)
{
ssize_t bytes = libssh2_channel_read_ex(channel, SSH_EXTENDED_DATA_STDERR, buffer.data(), buffer.size());
if (bytes > 0)
{
stderrData.append(buffer.data(), static_cast<size_t>(bytes));
}
else if (bytes == LIBSSH2_ERROR_EAGAIN)
{
continue;
}
else
{
break;
}
}
libssh2_channel_close(channel);
libssh2_channel_wait_closed(channel);
int exitStatus = libssh2_channel_get_exit_status(channel);
libssh2_channel_free(channel);
const unsigned char* fingerprint = reinterpret_cast<const unsigned char*>(libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256));
std::string fingerprintHex = makeHex(fingerprint, fingerprint ? 32 : 0);
libssh2_session_disconnect(session, "Normal Shutdown");
libssh2_session_free(session);
closeSocket(socket);
std::ostringstream oss;
if (!fingerprintHex.empty())
{
oss << "HostKey SHA256: " << fingerprintHex << "\n";
}
oss << "STDOUT:\n" << stdoutData;
if (!stdoutData.empty() && stdoutData.back() != '\n')
{
oss << '\n';
}
if (!stderrData.empty())
{
oss << "STDERR:\n" << stderrData;
if (stderrData.back() != '\n')
{
oss << '\n';
}
}
oss << "ExitCode: " << exitStatus << "\n";
result = oss.str();
return 0;
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include "ModuleCmd.hpp"
#include <string>
#include <vector>
class SshExec : public ModuleCmd
{
public:
SshExec();
~SshExec();
std::string getInfo();
int init(std::vector<std::string>& splitedCmd, C2Message& c2Message);
int process(C2Message& c2Message, C2Message& c2RetMessage);
int errorCodeToMsg(const C2Message& c2RetMessage, std::string& errorMsg);
int osCompatibility()
{
return OS_LINUX | OS_WINDOWS;
}
static constexpr int ErrorLibssh2Init = 1;
static constexpr int ErrorSocketInit = 2;
static constexpr int ErrorResolve = 3;
static constexpr int ErrorConnect = 4;
static constexpr int ErrorSessionInit = 5;
static constexpr int ErrorHandshake = 6;
static constexpr int ErrorAuthentication = 7;
static constexpr int ErrorChannelOpen = 8;
static constexpr int ErrorExecute = 9;
private:
struct Parameters
{
std::string host;
std::string port = "22";
std::string username;
std::string password;
std::string command;
};
std::string packParameters(const Parameters& params) const;
Parameters unpackParameters(const std::string& data) const;
int executeSshCommand(const Parameters& params, std::string& result) const;
};
#ifdef _WIN32
extern "C" __declspec(dllexport) SshExec* SshExecConstructor();
#else
extern "C" __attribute__((visibility("default"))) SshExec* SshExecConstructor();
#endif
+43
View File
@@ -0,0 +1,43 @@
#include "../SshExec.hpp"
#include <iostream>
#include <memory>
#include <vector>
int main()
{
{
std::unique_ptr<SshExec> module = std::make_unique<SshExec>();
std::vector<std::string> cmd = {"sshExec", "-h", "192.168.1.21", "-u", "kali", "-p", "kali", "whoami"};
C2Message message;
C2Message ret;
module->init(cmd, message);
module->process(message, ret);
std::string errMsg;
module->errorCodeToMsg(ret, errMsg);
std::cout << ret.returnvalue() << std::endl;
std::cerr << errMsg << std::endl;
}
{
std::unique_ptr<SshExec> module = std::make_unique<SshExec>();
std::vector<std::string> cmd = {"sshExec", "-h", "192.168.1.12", "-u", "root", "-p", "root", "whoami"};
C2Message message;
C2Message ret;
module->init(cmd, message);
module->process(message, ret);
std::string errMsg;
module->errorCodeToMsg(ret, errMsg);
std::cout << ret.returnvalue() << std::endl;
std::cerr << errMsg << std::endl;
}
std::cout << "Finish" << std::endl;
return 0;
}