diff --git a/CMakeLists.txt b/CMakeLists.txt index ddc19dd..b63e617 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/libssh2.pc.in b/libssh2.pc.in new file mode 100644 index 0000000..9663816 --- /dev/null +++ b/libssh2.pc.in @@ -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} diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 5141a14..594a135 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -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) diff --git a/modules/SshExec/CMakeLists.txt b/modules/SshExec/CMakeLists.txt new file mode 100644 index 0000000..5177a87 --- /dev/null +++ b/modules/SshExec/CMakeLists.txt @@ -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 + $ "${CMAKE_SOURCE_DIR}/Release/Modules/$") + +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 + $ "${CMAKE_SOURCE_DIR}/Tests/$") + add_test(NAME testsSshExec COMMAND "${CMAKE_SOURCE_DIR}/Tests/$") +endif() diff --git a/modules/SshExec/SshExec.cpp b/modules/SshExec/SshExec.cpp new file mode 100644 index 0000000..18284ed --- /dev/null +++ b/modules/SshExec/SshExec.cpp @@ -0,0 +1,592 @@ +#include "SshExec.hpp" + +#include "Common.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef _WIN32 + #include + #include +#else + #include + #include + #include +#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(errorCode), lang, reinterpret_cast(&lpMsgBuf), 0, nullptr); + std::string message; + if (size && lpMsgBuf) + { + message.assign(static_cast(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(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 Target hostname or IP.\n"; + oss << " -P, --port SSH port (default 22).\n"; + oss << " -u, --user Username.\n"; + oss << " -p, --password Password.\n"; + oss << " -c, --command 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 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& splitedCmd, C2Message& c2Message) +{ +#if defined(BUILD_TEAMSERVER) || defined(BUILD_TESTS) || defined(C2CORE_BUILD_TESTS) + std::vector 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 addrList(rawInfo, freeaddrinfo); + + SocketHandle socket = InvalidSocket; + for (addrinfo* ptr = addrList.get(); ptr != nullptr; ptr = ptr->ai_next) + { + SocketHandle candidate = static_cast(::socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol)); + if (candidate == InvalidSocket) + { + continue; + } + + if (::connect(candidate, ptr->ai_addr, static_cast(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 buffer{}; + + while (true) + { + ssize_t bytes = libssh2_channel_read(channel, buffer.data(), buffer.size()); + if (bytes > 0) + { + stdoutData.append(buffer.data(), static_cast(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(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(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; +} diff --git a/modules/SshExec/SshExec.hpp b/modules/SshExec/SshExec.hpp new file mode 100644 index 0000000..f6fe882 --- /dev/null +++ b/modules/SshExec/SshExec.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "ModuleCmd.hpp" + +#include +#include + +class SshExec : public ModuleCmd +{ +public: + SshExec(); + ~SshExec(); + + std::string getInfo(); + + int init(std::vector& 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 diff --git a/modules/SshExec/tests/testsSshExec.cpp b/modules/SshExec/tests/testsSshExec.cpp new file mode 100644 index 0000000..1324b31 --- /dev/null +++ b/modules/SshExec/tests/testsSshExec.cpp @@ -0,0 +1,43 @@ +#include "../SshExec.hpp" + +#include +#include +#include + +int main() +{ + { + std::unique_ptr module = std::make_unique(); + std::vector 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 module = std::make_unique(); + std::vector 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; +}