mirror of
https://github.com/maxDcb/C2TeamServer
synced 2026-06-06 16:14:27 +00:00
384 lines
10 KiB
Python
384 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
EXPECTED_TEAMSERVER_FILES = (
|
|
"TeamServer",
|
|
"TeamServerConfig.json",
|
|
"auth_credentials.json",
|
|
"localhost.crt",
|
|
"localhost.key",
|
|
"rootCA.crt",
|
|
"server.crt",
|
|
"server.key",
|
|
)
|
|
|
|
EXPECTED_TEAMSERVER_MODULES = (
|
|
"libAssemblyExec.so",
|
|
"libCat.so",
|
|
"libChangeDirectory.so",
|
|
"libChisel.so",
|
|
"libCimExec.so",
|
|
"libCoff.so",
|
|
"libDcomExec.so",
|
|
"libDotnetExec.so",
|
|
"libDownload.so",
|
|
"libEnumerateRdpSessions.so",
|
|
"libEnumerateShares.so",
|
|
"libEvasion.so",
|
|
"libGetEnv.so",
|
|
"libInject.so",
|
|
"libIpConfig.so",
|
|
"libKerberosUseTicket.so",
|
|
"libKeyLogger.so",
|
|
"libKillProcess.so",
|
|
"libListDirectory.so",
|
|
"libListProcesses.so",
|
|
"libMakeToken.so",
|
|
"libMiniDump.so",
|
|
"libMkDir.so",
|
|
"libNetstat.so",
|
|
"libPowershell.so",
|
|
"libPrintWorkingDirectory.so",
|
|
"libPsExec.so",
|
|
"libPwSh.so",
|
|
"libRegistry.so",
|
|
"libRemove.so",
|
|
"libRev2self.so",
|
|
"libRun.so",
|
|
"libScreenShot.so",
|
|
"libScript.so",
|
|
"libShell.so",
|
|
"libSpawnAs.so",
|
|
"libSshExec.so",
|
|
"libStealToken.so",
|
|
"libTaskScheduler.so",
|
|
"libTree.so",
|
|
"libUpload.so",
|
|
"libWhoami.so",
|
|
"libWinRM.so",
|
|
"libWmiExec.so",
|
|
"libReversePortForward.so",
|
|
)
|
|
|
|
EXPECTED_WINDOWS_ARCHES = (
|
|
"x86",
|
|
"x64",
|
|
"arm64",
|
|
)
|
|
|
|
EXPECTED_LINUX_ARCHES = (
|
|
"x64",
|
|
)
|
|
|
|
EXPECTED_WINDOWS_BEACONS = (
|
|
"BeaconDns.exe",
|
|
"BeaconDnsDll.dll",
|
|
"BeaconGithub.exe",
|
|
"BeaconGithubDll.dll",
|
|
"BeaconHttp.exe",
|
|
"BeaconHttpDll.dll",
|
|
"BeaconSmb.exe",
|
|
"BeaconSmbDll.dll",
|
|
"BeaconTcp.exe",
|
|
"BeaconTcpDll.dll",
|
|
)
|
|
|
|
EXPECTED_WINDOWS_MODULES = (
|
|
"AssemblyExec.dll",
|
|
"Cat.dll",
|
|
"ChangeDirectory.dll",
|
|
"Chisel.dll",
|
|
"CimExec.dll",
|
|
"Coff.dll",
|
|
"DcomExec.dll",
|
|
"DotnetExec.dll",
|
|
"Download.dll",
|
|
"EnumerateRdpSessions.dll",
|
|
"EnumerateShares.dll",
|
|
"Evasion.dll",
|
|
"GetEnv.dll",
|
|
"Inject.dll",
|
|
"IpConfig.dll",
|
|
"KerberosUseTicket.dll",
|
|
"KeyLogger.dll",
|
|
"KillProcess.dll",
|
|
"ListDirectory.dll",
|
|
"ListProcesses.dll",
|
|
"MakeToken.dll",
|
|
"MiniDump.dll",
|
|
"MkDir.dll",
|
|
"Netstat.dll",
|
|
"Powershell.dll",
|
|
"PrintWorkingDirectory.dll",
|
|
"PsExec.dll",
|
|
"PwSh.dll",
|
|
"Registry.dll",
|
|
"Remove.dll",
|
|
"Rev2self.dll",
|
|
"Run.dll",
|
|
"ScreenShot.dll",
|
|
"Script.dll",
|
|
"Shell.dll",
|
|
"SpawnAs.dll",
|
|
"SshExec.dll",
|
|
"StealToken.dll",
|
|
"TaskScheduler.dll",
|
|
"Tree.dll",
|
|
"Upload.dll",
|
|
"Whoami.dll",
|
|
"WinRM.dll",
|
|
"WmiExec.dll",
|
|
"ReversePortForward.dll",
|
|
)
|
|
|
|
EXPECTED_LINUX_BEACONS = (
|
|
"BeaconDns",
|
|
"BeaconGithub",
|
|
"BeaconHttp",
|
|
"BeaconSmb",
|
|
"BeaconTcp",
|
|
)
|
|
|
|
EXPECTED_LINUX_MODULES = EXPECTED_TEAMSERVER_MODULES
|
|
|
|
EXPECTED_COMMAND_SPECS_COMMON = (
|
|
"sleep.json",
|
|
"end.json",
|
|
"help.json",
|
|
"listener.json",
|
|
"listModule.json",
|
|
"loadModule.json",
|
|
"unloadModule.json",
|
|
)
|
|
|
|
EXPECTED_COMMAND_SPECS_MODULES = (
|
|
"assemblyExec.json",
|
|
"cat.json",
|
|
"cd.json",
|
|
"chisel.json",
|
|
"cimExec.json",
|
|
"coffLoader.json",
|
|
"dcomExec.json",
|
|
"dotnetExec.json",
|
|
"download.json",
|
|
"enumerateRdpSessions.json",
|
|
"enumerateShares.json",
|
|
"evasion.json",
|
|
"getEnv.json",
|
|
"inject.json",
|
|
"ipConfig.json",
|
|
"kerberosUseTicket.json",
|
|
"keyLogger.json",
|
|
"killProcess.json",
|
|
"ls.json",
|
|
"makeToken.json",
|
|
"miniDump.json",
|
|
"mkDir.json",
|
|
"netstat.json",
|
|
"ps.json",
|
|
"psExec.json",
|
|
"pwSh.json",
|
|
"powershell.json",
|
|
"pwd.json",
|
|
"registry.json",
|
|
"remove.json",
|
|
"rev2self.json",
|
|
"reversePortForward.json",
|
|
"run.json",
|
|
"screenShot.json",
|
|
"shell.json",
|
|
"script.json",
|
|
"spawnAs.json",
|
|
"sshExec.json",
|
|
"stealToken.json",
|
|
"taskScheduler.json",
|
|
"tree.json",
|
|
"upload.json",
|
|
"whoami.json",
|
|
"winRm.json",
|
|
"wmiExec.json",
|
|
)
|
|
|
|
|
|
class ValidationError(RuntimeError):
|
|
pass
|
|
|
|
|
|
def _relative_files(root: Path) -> set[str]:
|
|
return {
|
|
path.relative_to(root).as_posix()
|
|
for path in root.rglob("*")
|
|
if path.is_file()
|
|
}
|
|
|
|
|
|
def _require_non_empty_file(path: Path) -> None:
|
|
if not path.is_file():
|
|
raise ValidationError(f"Missing required file: {path}")
|
|
if path.stat().st_size == 0:
|
|
raise ValidationError(f"Required file is empty: {path}")
|
|
|
|
|
|
def _require_executable(path: Path) -> None:
|
|
_require_non_empty_file(path)
|
|
if os.name != "nt" and not os.access(path, os.X_OK):
|
|
raise ValidationError(f"Required file is not executable: {path}")
|
|
|
|
|
|
def _require_directory_exact(root: Path, expected_files: tuple[str, ...]) -> None:
|
|
if not root.is_dir():
|
|
raise ValidationError(f"Missing required directory: {root}")
|
|
|
|
expected = set(expected_files)
|
|
actual = _relative_files(root)
|
|
missing = sorted(expected - actual)
|
|
unexpected = sorted(actual - expected)
|
|
|
|
if missing:
|
|
raise ValidationError(f"{root} is missing expected file(s): {', '.join(missing)}")
|
|
if unexpected:
|
|
raise ValidationError(f"{root} contains unexpected file(s): {', '.join(unexpected)}")
|
|
|
|
for relative in expected_files:
|
|
_require_non_empty_file(root / relative)
|
|
|
|
|
|
def _require_arch_directories_exact(
|
|
root: Path,
|
|
expected_arches: tuple[str, ...],
|
|
expected_files: tuple[str, ...],
|
|
) -> None:
|
|
if not root.is_dir():
|
|
raise ValidationError(f"Missing required directory: {root}")
|
|
|
|
expected = set(expected_arches)
|
|
actual_dirs = {path.name for path in root.iterdir() if path.is_dir()}
|
|
actual_files = {path.name for path in root.iterdir() if path.is_file()}
|
|
|
|
missing = sorted(expected - actual_dirs)
|
|
unexpected_dirs = sorted(actual_dirs - expected)
|
|
unexpected_files = sorted(actual_files)
|
|
|
|
if missing:
|
|
raise ValidationError(f"{root} is missing expected arch directories: {', '.join(missing)}")
|
|
if unexpected_dirs:
|
|
raise ValidationError(f"{root} contains unexpected arch directories: {', '.join(unexpected_dirs)}")
|
|
if unexpected_files:
|
|
raise ValidationError(f"{root} contains unexpected file(s): {', '.join(unexpected_files)}")
|
|
|
|
for arch in expected_arches:
|
|
_require_directory_exact(root / arch, expected_files)
|
|
|
|
|
|
def validate_base_release(release_root: Path) -> None:
|
|
if not release_root.is_dir():
|
|
raise ValidationError(f"Release root does not exist: {release_root}")
|
|
|
|
teamserver_root = release_root / "TeamServer"
|
|
modules_root = release_root / "TeamServerModules"
|
|
command_specs_root = release_root / "CommandSpecs"
|
|
client_root = release_root / "Client"
|
|
|
|
if not teamserver_root.is_dir():
|
|
raise ValidationError(f"Missing TeamServer directory: {teamserver_root}")
|
|
for filename in EXPECTED_TEAMSERVER_FILES:
|
|
path = teamserver_root / filename
|
|
if filename == "TeamServer":
|
|
_require_executable(path)
|
|
else:
|
|
_require_non_empty_file(path)
|
|
|
|
if not (teamserver_root / "logs").is_dir():
|
|
raise ValidationError(f"Missing TeamServer logs directory: {teamserver_root / 'logs'}")
|
|
|
|
runtime_data_roots = ("data", "Tools", "Scripts", "UploadedArtifacts", "GeneratedArtifacts", "www")
|
|
packaged_data_roots = [name for name in runtime_data_roots if (release_root / name).exists()]
|
|
if packaged_data_roots:
|
|
raise ValidationError(
|
|
"Release staging contains runtime/operator data directories: "
|
|
+ ", ".join(packaged_data_roots)
|
|
)
|
|
|
|
_require_directory_exact(modules_root, EXPECTED_TEAMSERVER_MODULES)
|
|
|
|
for spec in EXPECTED_COMMAND_SPECS_COMMON:
|
|
_require_non_empty_file(command_specs_root / "common" / spec)
|
|
for spec in EXPECTED_COMMAND_SPECS_MODULES:
|
|
_require_non_empty_file(command_specs_root / "modules" / spec)
|
|
|
|
_require_non_empty_file(client_root / "README.md")
|
|
_require_non_empty_file(client_root / "pyproject.toml")
|
|
_require_non_empty_file(client_root / "requirements.txt")
|
|
_require_executable(client_root / "run-client.sh")
|
|
_require_non_empty_file(client_root / "run-client.ps1")
|
|
if not (client_root / "c2client_protocol" / "__init__.py").is_file():
|
|
raise ValidationError(
|
|
f"Missing required file: {client_root / 'c2client_protocol' / '__init__.py'}"
|
|
)
|
|
_require_non_empty_file(client_root / "c2client_protocol" / "TeamServerApi_pb2.py")
|
|
_require_non_empty_file(client_root / "c2client_protocol" / "TeamServerApi_pb2_grpc.py")
|
|
|
|
generated_noise = [
|
|
path for path in release_root.rglob("*") if path.name in {".gitignore", "__pycache__"}
|
|
]
|
|
if generated_noise:
|
|
raise ValidationError(
|
|
"Release staging contains generated/source-control noise: "
|
|
+ ", ".join(str(path) for path in generated_noise)
|
|
)
|
|
|
|
|
|
def validate_implants(release_root: Path) -> None:
|
|
_require_arch_directories_exact(
|
|
release_root / "WindowsBeacons",
|
|
EXPECTED_WINDOWS_ARCHES,
|
|
EXPECTED_WINDOWS_BEACONS,
|
|
)
|
|
_require_arch_directories_exact(
|
|
release_root / "WindowsModules",
|
|
EXPECTED_WINDOWS_ARCHES,
|
|
EXPECTED_WINDOWS_MODULES,
|
|
)
|
|
_require_arch_directories_exact(
|
|
release_root / "LinuxBeacons",
|
|
EXPECTED_LINUX_ARCHES,
|
|
EXPECTED_LINUX_BEACONS,
|
|
)
|
|
_require_arch_directories_exact(
|
|
release_root / "LinuxModules",
|
|
EXPECTED_LINUX_ARCHES,
|
|
EXPECTED_LINUX_MODULES,
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Validate a staged C2TeamServer release.")
|
|
parser.add_argument("--release-root", required=True, help="Path to the staged Release directory.")
|
|
parser.add_argument(
|
|
"--require-implants",
|
|
action="store_true",
|
|
help="Require and validate imported C2Implant/C2LinuxImplant assets.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
release_root = Path(args.release_root).resolve()
|
|
try:
|
|
validate_base_release(release_root)
|
|
if args.require_implants:
|
|
validate_implants(release_root)
|
|
except ValidationError as exc:
|
|
print(f"Release validation failed: {exc}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Validated release staging: {release_root}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|