Files

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())