ctf-agent 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +131 -0
- package/Dockerfile +192 -0
- package/README.md +522 -0
- package/agents/ctf-analyzer.md +58 -0
- package/agents/ctf-controller.md +151 -0
- package/agents/ctf-speedrun.md +74 -0
- package/bin/ctf-agent.js +119 -0
- package/docker-compose.yml +13 -0
- package/mcp_config.json +19 -0
- package/package.json +44 -0
- package/references/ctf-triage-ladder.md +68 -0
- package/references/exploit-databases.md +575 -0
- package/references/llm-safety-and-policy-compliance.md +173 -0
- package/references/multi-agent-orchestration-and-policy-routing.md +278 -0
- package/references/security-events-and-intelligence.md +566 -0
- package/references/version-matrix.md +59 -0
- package/rules/ctf-execution-rules.md +44 -0
- package/rules/ctf-safety-framing-rules.md +140 -0
- package/rules/flag-validation-rules.md +25 -0
- package/scripts/__init__.py +1 -0
- package/scripts/ctf-tools.lock +205 -0
- package/scripts/ctf_init.py +1026 -0
- package/scripts/ctf_update.py +583 -0
- package/scripts/ctfd_client.py +81 -0
- package/scripts/cve_lookup.py +453 -0
- package/scripts/extract_flags.py +155 -0
- package/scripts/install_as_agent.py +278 -0
- package/scripts/install_ctf_tools.sh +603 -0
- package/scripts/parallel_triage.py +395 -0
- package/scripts/prompt_policy_sanitizer.py +283 -0
- package/scripts/scope_guard.py +444 -0
- package/scripts/skill_validator.py +504 -0
- package/scripts/workspace_cleaner.py +141 -0
- package/skills/ctf-ai-ml/SKILL.md +120 -0
- package/skills/ctf-ai-ml/adversarial-ml.md +623 -0
- package/skills/ctf-ai-ml/llm-attacks.md +487 -0
- package/skills/ctf-ai-ml/model-attacks.md +422 -0
- package/skills/ctf-crypto/SKILL.md +320 -0
- package/skills/ctf-crypto/advanced-math.md +798 -0
- package/skills/ctf-crypto/classic-ciphers.md +651 -0
- package/skills/ctf-crypto/ecc-attacks.md +347 -0
- package/skills/ctf-crypto/exotic-crypto-2.md +380 -0
- package/skills/ctf-crypto/exotic-crypto.md +528 -0
- package/skills/ctf-crypto/historical.md +113 -0
- package/skills/ctf-crypto/lattice-and-lwe.md +524 -0
- package/skills/ctf-crypto/modern-ciphers-2.md +563 -0
- package/skills/ctf-crypto/modern-ciphers-3.md +453 -0
- package/skills/ctf-crypto/modern-ciphers.md +649 -0
- package/skills/ctf-crypto/prng-attacks.md +257 -0
- package/skills/ctf-crypto/prng.md +664 -0
- package/skills/ctf-crypto/rsa-attacks-2.md +792 -0
- package/skills/ctf-crypto/rsa-attacks.md +487 -0
- package/skills/ctf-crypto/stream-ciphers.md +390 -0
- package/skills/ctf-crypto/zkp-and-advanced.md +456 -0
- package/skills/ctf-forensics/3d-printing.md +121 -0
- package/skills/ctf-forensics/SKILL.md +379 -0
- package/skills/ctf-forensics/disk-advanced.md +497 -0
- package/skills/ctf-forensics/disk-and-memory.md +491 -0
- package/skills/ctf-forensics/disk-recovery.md +699 -0
- package/skills/ctf-forensics/linux-forensics.md +511 -0
- package/skills/ctf-forensics/network-advanced.md +583 -0
- package/skills/ctf-forensics/network.md +645 -0
- package/skills/ctf-forensics/peripheral-capture.md +287 -0
- package/skills/ctf-forensics/signals-and-hardware.md +713 -0
- package/skills/ctf-forensics/steganography.md +694 -0
- package/skills/ctf-forensics/stego-advanced-2.md +475 -0
- package/skills/ctf-forensics/stego-advanced.md +481 -0
- package/skills/ctf-forensics/stego-image.md +691 -0
- package/skills/ctf-forensics/windows.md +625 -0
- package/skills/ctf-malware/SKILL.md +181 -0
- package/skills/ctf-malware/c2-and-protocols.md +274 -0
- package/skills/ctf-malware/pe-and-dotnet.md +108 -0
- package/skills/ctf-malware/scripts-and-obfuscation.md +449 -0
- package/skills/ctf-misc/SKILL.md +498 -0
- package/skills/ctf-misc/bashjails.md +323 -0
- package/skills/ctf-misc/ctfd-navigation.md +465 -0
- package/skills/ctf-misc/dns.md +255 -0
- package/skills/ctf-misc/encodings-advanced.md +504 -0
- package/skills/ctf-misc/encodings.md +431 -0
- package/skills/ctf-misc/games-and-vms-2.md +254 -0
- package/skills/ctf-misc/games-and-vms-3.md +690 -0
- package/skills/ctf-misc/games-and-vms-4.md +229 -0
- package/skills/ctf-misc/games-and-vms.md +529 -0
- package/skills/ctf-misc/linux-privesc.md +333 -0
- package/skills/ctf-misc/pyjails.md +671 -0
- package/skills/ctf-misc/rf-sdr.md +91 -0
- package/skills/ctf-osint/SKILL.md +198 -0
- package/skills/ctf-osint/geolocation-and-media.md +464 -0
- package/skills/ctf-osint/social-media.md +312 -0
- package/skills/ctf-osint/web-and-dns.md +341 -0
- package/skills/ctf-pwn/SKILL.md +214 -0
- package/skills/ctf-pwn/advanced-exploits-2.md +579 -0
- package/skills/ctf-pwn/advanced-exploits-3.md +598 -0
- package/skills/ctf-pwn/advanced-exploits-4.md +590 -0
- package/skills/ctf-pwn/advanced-exploits-5.md +119 -0
- package/skills/ctf-pwn/advanced-exploits.md +773 -0
- package/skills/ctf-pwn/advanced.md +326 -0
- package/skills/ctf-pwn/field-notes.md +245 -0
- package/skills/ctf-pwn/format-string.md +694 -0
- package/skills/ctf-pwn/heap-fsop.md +285 -0
- package/skills/ctf-pwn/heap-techniques-2.md +333 -0
- package/skills/ctf-pwn/heap-techniques.md +513 -0
- package/skills/ctf-pwn/kernel-bypass.md +421 -0
- package/skills/ctf-pwn/kernel-techniques.md +366 -0
- package/skills/ctf-pwn/kernel.md +636 -0
- package/skills/ctf-pwn/overflow-basics.md +611 -0
- package/skills/ctf-pwn/rop-advanced.md +725 -0
- package/skills/ctf-pwn/rop-and-shellcode.md +659 -0
- package/skills/ctf-pwn/sandbox-escape.md +313 -0
- package/skills/ctf-reverse/SKILL.md +163 -0
- package/skills/ctf-reverse/anti-analysis-ctf.md +204 -0
- package/skills/ctf-reverse/anti-analysis.md +693 -0
- package/skills/ctf-reverse/field-notes.md +376 -0
- package/skills/ctf-reverse/languages-compiled.md +666 -0
- package/skills/ctf-reverse/languages-platforms.md +592 -0
- package/skills/ctf-reverse/languages.md +553 -0
- package/skills/ctf-reverse/patterns-ctf-2.md +397 -0
- package/skills/ctf-reverse/patterns-ctf-3.md +797 -0
- package/skills/ctf-reverse/patterns-ctf.md +670 -0
- package/skills/ctf-reverse/patterns-runtime.md +274 -0
- package/skills/ctf-reverse/patterns.md +572 -0
- package/skills/ctf-reverse/platforms-hardware.md +387 -0
- package/skills/ctf-reverse/platforms.md +664 -0
- package/skills/ctf-reverse/tools-advanced-2.md +421 -0
- package/skills/ctf-reverse/tools-advanced.md +407 -0
- package/skills/ctf-reverse/tools-dynamic.md +679 -0
- package/skills/ctf-reverse/tools-emulation.md +319 -0
- package/skills/ctf-reverse/tools.md +573 -0
- package/skills/ctf-web/SKILL.md +153 -0
- package/skills/ctf-web/auth-and-access-2.md +82 -0
- package/skills/ctf-web/auth-and-access.md +783 -0
- package/skills/ctf-web/auth-infra.md +321 -0
- package/skills/ctf-web/auth-jwt.md +186 -0
- package/skills/ctf-web/client-side-advanced.md +739 -0
- package/skills/ctf-web/client-side.md +529 -0
- package/skills/ctf-web/cves.md +373 -0
- package/skills/ctf-web/field-notes.md +482 -0
- package/skills/ctf-web/node-and-prototype.md +200 -0
- package/skills/ctf-web/server-side-2.md +337 -0
- package/skills/ctf-web/server-side-advanced-2.md +559 -0
- package/skills/ctf-web/server-side-advanced-3.md +125 -0
- package/skills/ctf-web/server-side-advanced-4.md +480 -0
- package/skills/ctf-web/server-side-advanced.md +378 -0
- package/skills/ctf-web/server-side-deser.md +443 -0
- package/skills/ctf-web/server-side-exec-2.md +799 -0
- package/skills/ctf-web/server-side-exec.md +457 -0
- package/skills/ctf-web/server-side.md +629 -0
- package/skills/ctf-web/sql-injection.md +790 -0
- package/skills/ctf-web/web3.md +374 -0
- package/skills/ctf-writeup/SKILL.md +90 -0
- package/skills/solve-challenge/SKILL.md +269 -0
- package/skills.json +16 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CTF-Agent Machine-Enforced Scope Guard
|
|
4
|
+
Enforces runtime boundary compliance for Security Context Objects (SCO) and Task Envelopes (TE).
|
|
5
|
+
Validates targets against authorized educational sandboxes and blocks prohibited actions.
|
|
6
|
+
Includes strict anti-SSRF, IP obfuscation detection, and target category classification.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import re
|
|
12
|
+
import ipaddress
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
19
|
+
|
|
20
|
+
# Safe UTF-8 output on Windows consoles
|
|
21
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
22
|
+
try:
|
|
23
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
24
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TargetCategory(str, Enum):
|
|
32
|
+
LOCAL_FILE = "local_file"
|
|
33
|
+
PRIVATE_LAB = "private_lab"
|
|
34
|
+
CHALLENGE_INSTANCE = "challenge_instance"
|
|
35
|
+
PLATFORM_PORTAL = "platform_portal"
|
|
36
|
+
CUSTOM_AUTHORIZED = "custom_authorized"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Allowed protocols for CTF target interactions
|
|
40
|
+
PERMITTED_SCHEMES = {"http", "https", "tcp", "nc", "ssl", "ws", "wss", "ssh"}
|
|
41
|
+
|
|
42
|
+
# Strictly prohibited URL schemes (SSRF / LFI / local code execution risks)
|
|
43
|
+
DANGEROUS_SCHEMES = {"javascript", "file", "data", "gopher", "dict", "ldap", "vbscript"}
|
|
44
|
+
|
|
45
|
+
# Authorized Challenge Infrastructure (ephemeral instancers, challenge subdomains)
|
|
46
|
+
CHALLENGE_DOMAIN_PATTERNS = [
|
|
47
|
+
r"^(?:[a-zA-Z0-9-]+\.)*chal{1,2}s?\.io$",
|
|
48
|
+
r"^(?:[a-zA-Z0-9-]+\.)*chal\.pw$",
|
|
49
|
+
r"^(?:[a-zA-Z0-9-]+\.)*picoctf\.net$",
|
|
50
|
+
r"^challenge\.flagyard\.com$",
|
|
51
|
+
r"^play\.flagyard\.com$",
|
|
52
|
+
r"^(?:[a-zA-Z0-9-]+\.)*lab\.hackthebox\.com$",
|
|
53
|
+
r"^lab\.hackthebox\.com$",
|
|
54
|
+
r"^app\.hackthebox\.com$",
|
|
55
|
+
r"^(?:[a-zA-Z0-9-]+\.)*web-security-academy\.net$",
|
|
56
|
+
r"^(?:[a-zA-Z0-9-]+\.)*root-me\.org$",
|
|
57
|
+
r"^(?:[a-zA-Z0-9-]+\.)*cyberdefenders\.org$",
|
|
58
|
+
r"^(?:[a-zA-Z0-9-]+\.)*pwni\.ng$",
|
|
59
|
+
r"^(?:[a-zA-Z0-9-]+\.)*ctf\.su$",
|
|
60
|
+
r"^(?:[a-zA-Z0-9-]+\.)*seccon\.games$",
|
|
61
|
+
r"^(?:[a-zA-Z0-9-]+\.)*kctf\.dev$",
|
|
62
|
+
r"^(?:[a-zA-Z0-9-]+\.)*instancer\.[a-zA-Z0-9-.]+$",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
# Authorized Platform Portals (scoreboards / account portals)
|
|
66
|
+
PLATFORM_PORTAL_PATTERNS = [
|
|
67
|
+
r"^(?:www\.)?picoctf\.org$",
|
|
68
|
+
r"^play\.picoctf\.org$",
|
|
69
|
+
r"^(?:[a-zA-Z0-9-]+\.)*ctfd\.io$",
|
|
70
|
+
r"^ctfd\.io$",
|
|
71
|
+
r"^(?:www\.)?hackthebox\.(?:com|eu)$",
|
|
72
|
+
r"^(?:www\.)?tryhackme\.com$",
|
|
73
|
+
r"^(?:www\.)?flagyard\.com$",
|
|
74
|
+
r"^(?:www\.)?overthewire\.org$",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# Authorized Private / Lab TLDs and names
|
|
78
|
+
PRIVATE_LAB_DOMAIN_PATTERNS = [
|
|
79
|
+
r"^localhost$",
|
|
80
|
+
r"^(?:[a-zA-Z0-9-]+\.)*local$",
|
|
81
|
+
r"^(?:[a-zA-Z0-9-]+\.)*internal$",
|
|
82
|
+
r"^(?:[a-zA-Z0-9-]+\.)*lan$",
|
|
83
|
+
r"^(?:[a-zA-Z0-9-]+\.)*test$",
|
|
84
|
+
r"^(?:[a-zA-Z0-9-]+\.)*lab$",
|
|
85
|
+
r"^(?:[a-zA-Z0-9-]+\.)*ctf$",
|
|
86
|
+
r"^(?:[a-zA-Z0-9-]+\.)*htb$",
|
|
87
|
+
r"^(?:[a-zA-Z0-9-]+\.)*thm$",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
# Known SSRF / DNS wildcard evasion domains to block
|
|
91
|
+
DNS_REBINDING_DOMAINS = [
|
|
92
|
+
r"^(?:[a-zA-Z0-9-]+\.)*nip\.io$",
|
|
93
|
+
r"^(?:[a-zA-Z0-9-]+\.)*sslip\.io$",
|
|
94
|
+
r"^(?:[a-zA-Z0-9-]+\.)*xip\.io$",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Strictly prohibited command signatures
|
|
98
|
+
PROHIBITED_COMMAND_PATTERNS = [
|
|
99
|
+
(r"\brm\s+-[rR]f\s+/(?:\s|$|\*)", "Destructive command: Root filesystem deletion"),
|
|
100
|
+
(r"\bformat\s+[cC]:", "Destructive command: Drive format"),
|
|
101
|
+
(r"\bdel\s+/[fF]\s+/[sS]\s+/[qQ]\s+[cC]:\\Windows", "Destructive command: Windows system deletion"),
|
|
102
|
+
(r"\bmasscan\s+(?:0\.0\.0\.0/0|(?:\d{1,3}\.){3}\d{1,3}/(?:[0-7]|1[0-5]))", "Prohibited: Wide-area indiscriminate network scanning"),
|
|
103
|
+
(r"\bnmap\s+(?:-[^\s]+\s+)*(?:0\.0\.0\.0/0|(?:\d{1,3}\.){3}\d{1,3}/[1-9]\b)", "Prohibited: Wide-area indiscriminate network scanning"),
|
|
104
|
+
(r"\b(?:echo|cat)\s+.*>>\s+/(?:etc/cron|etc/init\.d)", "Prohibited: System-level persistence establishment"),
|
|
105
|
+
(r"\bReg(?:\.exe)?\s+ADD\s+HK(?:LM|CU)\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", "Prohibited: Windows autorun persistence"),
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ScopeViolationError(Exception):
|
|
110
|
+
"""Raised when an operation, target, or command violates educational sandbox scope."""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ScopeGuard:
|
|
115
|
+
"""Enforces target isolation, private network boundaries, anti-SSRF rules, and action restrictions."""
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def load_env_whitelist(workspace_path: Optional[Path] = None) -> List[str]:
|
|
119
|
+
"""Loads allowed hosts and URLs from .env or .env.example if present."""
|
|
120
|
+
allowed: List[str] = []
|
|
121
|
+
candidates = []
|
|
122
|
+
if workspace_path:
|
|
123
|
+
candidates.extend([workspace_path / ".env", workspace_path / ".env.example"])
|
|
124
|
+
candidates.extend([REPO_ROOT / ".env", REPO_ROOT / ".env.example"])
|
|
125
|
+
|
|
126
|
+
for env_file in candidates:
|
|
127
|
+
if env_file.is_file():
|
|
128
|
+
try:
|
|
129
|
+
for line in env_file.read_text(encoding="utf-8").splitlines():
|
|
130
|
+
line = line.strip()
|
|
131
|
+
if line and not line.startswith("#") and "=" in line:
|
|
132
|
+
k, v = line.split("=", 1)
|
|
133
|
+
k = k.strip().upper()
|
|
134
|
+
v = v.strip()
|
|
135
|
+
if k in ("TARGET_HOST", "TARGET_URL", "CTFD_URL") and v:
|
|
136
|
+
allowed.append(v)
|
|
137
|
+
except Exception:
|
|
138
|
+
pass
|
|
139
|
+
return allowed
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def check_ip_obfuscation(cls, host: str) -> Tuple[bool, Optional[str]]:
|
|
143
|
+
"""
|
|
144
|
+
Detects alternate IP representations (integer, hex, octal, dword) used to bypass scope/SSRF checks.
|
|
145
|
+
Returns (is_obfuscated, reason).
|
|
146
|
+
"""
|
|
147
|
+
raw = host.strip()
|
|
148
|
+
|
|
149
|
+
# 1. Decimal integer representation (e.g. 2130706433)
|
|
150
|
+
if raw.isdigit() and len(raw) > 3:
|
|
151
|
+
try:
|
|
152
|
+
num = int(raw)
|
|
153
|
+
if 0 <= num <= 4294967295:
|
|
154
|
+
return True, f"Obfuscated integer IP representation rejected: {raw}"
|
|
155
|
+
except ValueError:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
# 2. Hexadecimal representation (e.g. 0x7f000001 or dotted 0x7f.0.0.1)
|
|
159
|
+
if raw.lower().startswith("0x") or re.search(r"\b0x[0-9a-fA-F]+\b", raw):
|
|
160
|
+
return True, f"Obfuscated hexadecimal IP representation rejected: {raw}"
|
|
161
|
+
|
|
162
|
+
# 3. Octal representation (e.g. 017700000001 or dotted octal 0177.0.0.1)
|
|
163
|
+
if re.match(r"^0[0-7]+$", raw) and len(raw) > 3:
|
|
164
|
+
return True, f"Obfuscated octal IP representation rejected: {raw}"
|
|
165
|
+
|
|
166
|
+
# 4. Dotted representation with octal octets (e.g. 0177.0.0.1, 127.000.000.001)
|
|
167
|
+
if "." in raw:
|
|
168
|
+
parts = raw.split(".")
|
|
169
|
+
if len(parts) == 4 and all(p.isdigit() for p in parts):
|
|
170
|
+
for p in parts:
|
|
171
|
+
if len(p) > 1 and p.startswith("0"):
|
|
172
|
+
return True, f"Obfuscated octal-formatted dotted IP rejected: {raw}"
|
|
173
|
+
|
|
174
|
+
return False, None
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def validate_target(
|
|
178
|
+
cls,
|
|
179
|
+
target: str,
|
|
180
|
+
workspace_path: Optional[Path] = None,
|
|
181
|
+
) -> Tuple[bool, str]:
|
|
182
|
+
"""
|
|
183
|
+
Validates whether a target is inside an authorized CTF educational sandbox.
|
|
184
|
+
Backwards-compatible tuple (is_authorized, reason_string).
|
|
185
|
+
"""
|
|
186
|
+
is_auth, reason, _ = cls.validate_target_with_category(target, workspace_path=workspace_path)
|
|
187
|
+
return is_auth, reason
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def validate_target_with_category(
|
|
191
|
+
cls,
|
|
192
|
+
target: str,
|
|
193
|
+
workspace_path: Optional[Path] = None,
|
|
194
|
+
) -> Tuple[bool, str, TargetCategory]:
|
|
195
|
+
"""
|
|
196
|
+
Validates whether a target is inside an authorized CTF educational sandbox.
|
|
197
|
+
Returns:
|
|
198
|
+
(is_authorized, reason_string, target_category)
|
|
199
|
+
"""
|
|
200
|
+
if not target or not target.strip():
|
|
201
|
+
return False, "Target is empty", TargetCategory.PRIVATE_LAB
|
|
202
|
+
|
|
203
|
+
raw_target = target.strip()
|
|
204
|
+
|
|
205
|
+
# Check for dangerous schemes upfront
|
|
206
|
+
lower_target = raw_target.lower()
|
|
207
|
+
for d_scheme in DANGEROUS_SCHEMES:
|
|
208
|
+
if lower_target.startswith(f"{d_scheme}:"):
|
|
209
|
+
return False, f"Scope Violation: Dangerous scheme '{d_scheme}:' is strictly prohibited.", TargetCategory.PRIVATE_LAB
|
|
210
|
+
|
|
211
|
+
# Check for protocol-relative URLs without scheme (//example.com)
|
|
212
|
+
if raw_target.startswith("//"):
|
|
213
|
+
return False, "Scope Violation: Protocol-relative URLs ('//') are not permitted.", TargetCategory.PRIVATE_LAB
|
|
214
|
+
|
|
215
|
+
# 1. Local filesystem path check (only when no protocol scheme is specified)
|
|
216
|
+
if "://" not in raw_target:
|
|
217
|
+
local_p = Path(raw_target)
|
|
218
|
+
# Must look like a real path or existing file
|
|
219
|
+
if (
|
|
220
|
+
local_p.exists()
|
|
221
|
+
or (local_p.is_absolute() and ("/" in raw_target or "\\" in raw_target))
|
|
222
|
+
or raw_target.startswith(("./", ".\\", "../", "..\\"))
|
|
223
|
+
or raw_target.endswith((".bin", ".elf", ".py", ".pcap", ".pcapng", ".exe", ".so", ".apk", ".zip", ".tar", ".gz"))
|
|
224
|
+
):
|
|
225
|
+
return True, f"Authorized local filesystem target: {local_p}", TargetCategory.LOCAL_FILE
|
|
226
|
+
|
|
227
|
+
# 2. URL parsing & scheme extraction
|
|
228
|
+
scheme = None
|
|
229
|
+
host = raw_target
|
|
230
|
+
port = None
|
|
231
|
+
|
|
232
|
+
if "://" in raw_target:
|
|
233
|
+
parsed = urllib.parse.urlsplit(raw_target)
|
|
234
|
+
scheme = parsed.scheme.lower()
|
|
235
|
+
if scheme not in PERMITTED_SCHEMES:
|
|
236
|
+
return False, f"Scope Violation: Prohibited or unsupported scheme '{scheme}'. Allowed: {', '.join(sorted(PERMITTED_SCHEMES))}", TargetCategory.PRIVATE_LAB
|
|
237
|
+
|
|
238
|
+
if not parsed.netloc and not parsed.hostname:
|
|
239
|
+
return False, f"Scope Violation: Malformed URL '{raw_target}' missing host.", TargetCategory.PRIVATE_LAB
|
|
240
|
+
|
|
241
|
+
host = parsed.hostname or ""
|
|
242
|
+
if not host and parsed.netloc:
|
|
243
|
+
# Handle host with port or bracketed IPv6
|
|
244
|
+
host = parsed.netloc.split("@")[-1] # Strip any userinfo
|
|
245
|
+
if ":" in host and not host.startswith("["):
|
|
246
|
+
host = host.split(":")[0]
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
port = parsed.port
|
|
250
|
+
except ValueError:
|
|
251
|
+
return False, f"Scope Violation: Invalid port specification in '{raw_target}'.", TargetCategory.PRIVATE_LAB
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
# Format: host:port or ip:port or pure host
|
|
255
|
+
if ":" in raw_target and not raw_target.startswith("["):
|
|
256
|
+
parts = raw_target.split(":")
|
|
257
|
+
if len(parts) == 2 and parts[1].split("/")[0].isdigit():
|
|
258
|
+
host = parts[0]
|
|
259
|
+
port_str = parts[1].split("/")[0]
|
|
260
|
+
try:
|
|
261
|
+
port = int(port_str)
|
|
262
|
+
except ValueError:
|
|
263
|
+
return False, f"Scope Violation: Invalid port '{port_str}'.", TargetCategory.PRIVATE_LAB
|
|
264
|
+
|
|
265
|
+
if not host:
|
|
266
|
+
return False, "Unable to extract valid host from target", TargetCategory.PRIVATE_LAB
|
|
267
|
+
|
|
268
|
+
# Strip brackets from IPv6 host
|
|
269
|
+
if host.startswith("[") and host.endswith("]"):
|
|
270
|
+
host = host[1:-1]
|
|
271
|
+
|
|
272
|
+
# 3. Port range verification (if specified)
|
|
273
|
+
if port is not None:
|
|
274
|
+
if not (1 <= port <= 65535):
|
|
275
|
+
return False, f"Scope Violation: Port {port} outside valid range (1-65535).", TargetCategory.PRIVATE_LAB
|
|
276
|
+
|
|
277
|
+
# 4. Anti-SSRF: Obfuscated IP check
|
|
278
|
+
is_obf, obf_reason = cls.check_ip_obfuscation(host)
|
|
279
|
+
if is_obf:
|
|
280
|
+
return False, f"Scope Violation: {obf_reason}", TargetCategory.PRIVATE_LAB
|
|
281
|
+
|
|
282
|
+
# 5. Anti-SSRF: DNS rebinding wildcard domains check (e.g. nip.io, sslip.io)
|
|
283
|
+
for rebind_pat in DNS_REBINDING_DOMAINS:
|
|
284
|
+
if re.match(rebind_pat, host, re.IGNORECASE):
|
|
285
|
+
return False, f"Scope Violation: Wildcard DNS rebinding domain '{host}' is prohibited.", TargetCategory.PRIVATE_LAB
|
|
286
|
+
|
|
287
|
+
# 6. Check Loopback hostnames
|
|
288
|
+
if host.lower() in ("localhost", "0.0.0.0"):
|
|
289
|
+
return True, f"Authorized loopback target: {host}", TargetCategory.PRIVATE_LAB
|
|
290
|
+
|
|
291
|
+
# 7. Check IP addresses (IPv4 & IPv6)
|
|
292
|
+
try:
|
|
293
|
+
ip_obj = ipaddress.ip_address(host)
|
|
294
|
+
|
|
295
|
+
if ip_obj.is_loopback:
|
|
296
|
+
return True, f"Authorized loopback IP: {ip_obj}", TargetCategory.PRIVATE_LAB
|
|
297
|
+
|
|
298
|
+
if ip_obj.is_private:
|
|
299
|
+
return True, f"Authorized private lab/container IP (RFC1918): {ip_obj}", TargetCategory.PRIVATE_LAB
|
|
300
|
+
|
|
301
|
+
# Check if public IP is explicitly whitelisted in .env
|
|
302
|
+
env_allowed = cls.load_env_whitelist(workspace_path)
|
|
303
|
+
if any(host == item or host in item for item in env_allowed):
|
|
304
|
+
return True, f"Authorized competition IP from .env whitelist: {host}", TargetCategory.CUSTOM_AUTHORIZED
|
|
305
|
+
|
|
306
|
+
return False, f"Scope Violation: Public IP {host} is outside authorized private sandbox.", TargetCategory.PRIVATE_LAB
|
|
307
|
+
|
|
308
|
+
except ValueError:
|
|
309
|
+
# Target is a domain name
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# 8. Check Challenge Infrastructure Patterns (highest priority for CTF instances)
|
|
313
|
+
for pat in CHALLENGE_DOMAIN_PATTERNS:
|
|
314
|
+
if re.match(pat, host, re.IGNORECASE):
|
|
315
|
+
return True, f"Authorized CTF challenge instance: {host}", TargetCategory.CHALLENGE_INSTANCE
|
|
316
|
+
|
|
317
|
+
# 9. Check Platform Portal Patterns
|
|
318
|
+
for pat in PLATFORM_PORTAL_PATTERNS:
|
|
319
|
+
if re.match(pat, host, re.IGNORECASE):
|
|
320
|
+
return True, f"Authorized CTF platform portal: {host}", TargetCategory.PLATFORM_PORTAL
|
|
321
|
+
|
|
322
|
+
# 10. Check Private / Lab Domain Patterns
|
|
323
|
+
for pat in PRIVATE_LAB_DOMAIN_PATTERNS:
|
|
324
|
+
if re.match(pat, host, re.IGNORECASE):
|
|
325
|
+
return True, f"Authorized private lab domain: {host}", TargetCategory.PRIVATE_LAB
|
|
326
|
+
|
|
327
|
+
# 11. Check .env whitelist for custom domains
|
|
328
|
+
env_allowed = cls.load_env_whitelist(workspace_path)
|
|
329
|
+
for item in env_allowed:
|
|
330
|
+
if host == item or item.endswith(f"://{host}") or f"://{host}:" in item:
|
|
331
|
+
return True, f"Authorized domain from .env configuration: {host}", TargetCategory.CUSTOM_AUTHORIZED
|
|
332
|
+
|
|
333
|
+
return False, (
|
|
334
|
+
f"Scope Violation: Domain '{host}' is not an authorized CTF challenge instance, platform, or private lab. "
|
|
335
|
+
f"Add to .env (TARGET_HOST/TARGET_URL) to authorize."
|
|
336
|
+
), TargetCategory.PRIVATE_LAB
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def validate_command(cls, command: str) -> Tuple[bool, str]:
|
|
340
|
+
"""
|
|
341
|
+
Validates command string against prohibited destructive, wide-scan, or persistence patterns.
|
|
342
|
+
"""
|
|
343
|
+
if not command or not command.strip():
|
|
344
|
+
return True, "Empty command"
|
|
345
|
+
|
|
346
|
+
cmd = command.strip()
|
|
347
|
+
for pattern, reason in PROHIBITED_COMMAND_PATTERNS:
|
|
348
|
+
if re.search(pattern, cmd, re.IGNORECASE):
|
|
349
|
+
return False, f"Prohibited Action Blocked: {reason} (matched rule: {pattern})"
|
|
350
|
+
|
|
351
|
+
return True, "Command passed scope guard validation"
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def create_security_context_object(
|
|
355
|
+
cls,
|
|
356
|
+
target: str,
|
|
357
|
+
category: str = "general",
|
|
358
|
+
mode: str = "speedrun",
|
|
359
|
+
workspace_path: Optional[Path] = None,
|
|
360
|
+
) -> Dict[str, Any]:
|
|
361
|
+
"""
|
|
362
|
+
Creates a formally validated Security Context Object (SCO) for agent orchestration.
|
|
363
|
+
Raises ScopeViolationError if the target fails scope verification.
|
|
364
|
+
"""
|
|
365
|
+
is_valid, reason, target_cat = cls.validate_target_with_category(target, workspace_path=workspace_path)
|
|
366
|
+
if not is_valid:
|
|
367
|
+
raise ScopeViolationError(reason)
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
"version": "1.2",
|
|
371
|
+
"type": "educational_ctf_challenge",
|
|
372
|
+
"scope": {
|
|
373
|
+
"target": target,
|
|
374
|
+
"category": category,
|
|
375
|
+
"target_type": target_cat.value,
|
|
376
|
+
"mode": mode,
|
|
377
|
+
"authorization_status": "authorized_sandbox",
|
|
378
|
+
"validation_reason": reason,
|
|
379
|
+
},
|
|
380
|
+
"environment": {
|
|
381
|
+
"isolated": True,
|
|
382
|
+
"sandbox_guarantee": True,
|
|
383
|
+
"production_targets": False,
|
|
384
|
+
},
|
|
385
|
+
"constraints": {
|
|
386
|
+
"max_subagent_depth": 1,
|
|
387
|
+
"persistence_allowed": False,
|
|
388
|
+
"destructive_actions_allowed": False,
|
|
389
|
+
"timeout_seconds": 60,
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def main():
|
|
395
|
+
parser = argparse.ArgumentParser(description="CTF-Agent Machine-Enforced Scope Guard v1.2")
|
|
396
|
+
parser.add_argument("--validate-target", help="Check if target is inside authorized educational sandbox")
|
|
397
|
+
parser.add_argument("--validate-cmd", help="Check if command contains prohibited or destructive actions")
|
|
398
|
+
parser.add_argument("--create-sco", help="Generate a verified Security Context Object for target")
|
|
399
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
400
|
+
|
|
401
|
+
args = parser.parse_args()
|
|
402
|
+
|
|
403
|
+
results: Dict[str, Any] = {}
|
|
404
|
+
|
|
405
|
+
if args.validate_target:
|
|
406
|
+
valid, reason, cat = ScopeGuard.validate_target_with_category(args.validate_target)
|
|
407
|
+
results = {
|
|
408
|
+
"target": args.validate_target,
|
|
409
|
+
"authorized": valid,
|
|
410
|
+
"category": cat.value,
|
|
411
|
+
"reason": reason,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
elif args.validate_cmd:
|
|
415
|
+
valid, reason = ScopeGuard.validate_command(args.validate_cmd)
|
|
416
|
+
results = {
|
|
417
|
+
"command": args.validate_cmd,
|
|
418
|
+
"authorized": valid,
|
|
419
|
+
"reason": reason,
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
elif args.create_sco:
|
|
423
|
+
try:
|
|
424
|
+
sco = ScopeGuard.create_security_context_object(args.create_sco)
|
|
425
|
+
results = {"status": "success", "sco": sco}
|
|
426
|
+
except ScopeViolationError as e:
|
|
427
|
+
results = {"status": "error", "error": str(e)}
|
|
428
|
+
|
|
429
|
+
else:
|
|
430
|
+
parser.print_help()
|
|
431
|
+
sys.exit(1)
|
|
432
|
+
|
|
433
|
+
if args.json:
|
|
434
|
+
print(json.dumps(results, indent=2))
|
|
435
|
+
else:
|
|
436
|
+
if results.get("authorized") or results.get("status") == "success":
|
|
437
|
+
print(f"[OK] Scope Guard: {results.get('reason', 'Security Context Created')}")
|
|
438
|
+
else:
|
|
439
|
+
print(f"[!] Scope Guard Refusal: {results.get('reason', results.get('error'))}", file=sys.stderr)
|
|
440
|
+
sys.exit(1)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
main()
|