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,453 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
CVE & Threat Intelligence Lookup Utility (v2.1)
|
|
4
|
+
Authoritative vulnerability intelligence pipeline for CTF Agents & Security Researchers:
|
|
5
|
+
- CVE List V5 / CVE Services API (cveawg.mitre.org)
|
|
6
|
+
- FIRST EPSS API (api.first.org)
|
|
7
|
+
- CISA KEV (Known Exploited Vulnerabilities) catalog with in-memory caching
|
|
8
|
+
- Exploit reference categorization (PoC, Exploit-DB, GitHub, Packet Storm)
|
|
9
|
+
- Decoupled CTF Challenge Relevance Layer
|
|
10
|
+
- Structured JSON output mode (--json)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
import json
|
|
18
|
+
import urllib.request
|
|
19
|
+
import urllib.error
|
|
20
|
+
from functools import lru_cache
|
|
21
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
22
|
+
|
|
23
|
+
# Safe UTF-8 output on Windows consoles
|
|
24
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
25
|
+
try:
|
|
26
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
27
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
USER_AGENT = "CTF-Agent-ThreatIntel/2.1"
|
|
32
|
+
TIMEOUT = 10
|
|
33
|
+
CVE_PATTERN = re.compile(r"^CVE-\d{4}-\d{4,}$")
|
|
34
|
+
|
|
35
|
+
EXPLOIT_URL_PATTERNS = [
|
|
36
|
+
re.compile(r"exploit-db\.com", re.I),
|
|
37
|
+
re.compile(r"packetstormsecurity\.com", re.I),
|
|
38
|
+
re.compile(r"github\.com/[^/]+/(?:poc|exploit|cve-)", re.I),
|
|
39
|
+
re.compile(r"0day\.today", re.I),
|
|
40
|
+
re.compile(r"seclists\.org/fulldisclosure", re.I),
|
|
41
|
+
re.compile(r"rapid7\.com/db", re.I),
|
|
42
|
+
re.compile(r"sploitus\.com", re.I),
|
|
43
|
+
re.compile(r"project-zero", re.I),
|
|
44
|
+
]
|
|
45
|
+
EXPLOIT_TAGS = {"exploit", "technical-description", "third-party-advisory"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_cve_id(cve_id: str) -> str:
|
|
49
|
+
"""Validates and normalizes CVE identifier format (CVE-YYYY-NNNN)."""
|
|
50
|
+
if not cve_id or not isinstance(cve_id, str) or not cve_id.strip():
|
|
51
|
+
raise ValueError(f"Invalid CVE identifier: '{cve_id}'. Expected format: CVE-YYYY-NNNN")
|
|
52
|
+
|
|
53
|
+
cleaned = cve_id.strip().upper()
|
|
54
|
+
if not cleaned.startswith("CVE-"):
|
|
55
|
+
cleaned = "CVE-" + cleaned
|
|
56
|
+
|
|
57
|
+
if not CVE_PATTERN.fullmatch(cleaned):
|
|
58
|
+
raise ValueError(f"Invalid CVE identifier: '{cve_id}'. Expected format: CVE-YYYY-NNNN")
|
|
59
|
+
|
|
60
|
+
return cleaned
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def fetch_json(url: str, retries: int = 2, backoff: float = 0.5) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
64
|
+
"""
|
|
65
|
+
Fetch JSON payload from a remote endpoint with retry logic and error classification.
|
|
66
|
+
Returns (data_dict, error_message).
|
|
67
|
+
"""
|
|
68
|
+
req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
|
|
69
|
+
|
|
70
|
+
for attempt in range(retries + 1):
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
|
73
|
+
content = resp.read().decode("utf-8")
|
|
74
|
+
return json.loads(content), None
|
|
75
|
+
except urllib.error.HTTPError as exc:
|
|
76
|
+
if exc.code == 404:
|
|
77
|
+
return None, f"HTTP 404: Resource not found at {url}"
|
|
78
|
+
if exc.code == 429 and attempt < retries:
|
|
79
|
+
time.sleep(backoff * (2 ** attempt))
|
|
80
|
+
continue
|
|
81
|
+
if exc.code in (500, 502, 503, 504) and attempt < retries:
|
|
82
|
+
time.sleep(backoff * (2 ** attempt))
|
|
83
|
+
continue
|
|
84
|
+
return None, f"HTTP error {exc.code} while fetching {url}: {exc.reason}"
|
|
85
|
+
except urllib.error.URLError as exc:
|
|
86
|
+
if attempt < retries:
|
|
87
|
+
time.sleep(backoff * (2 ** attempt))
|
|
88
|
+
continue
|
|
89
|
+
return None, f"Network error connecting to {url}: {exc.reason}"
|
|
90
|
+
except json.JSONDecodeError as exc:
|
|
91
|
+
return None, f"Malformed JSON response from {url}: {exc}"
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
return None, f"Unexpected error fetching {url}: {exc}"
|
|
94
|
+
|
|
95
|
+
return None, f"Failed to fetch {url} after {retries + 1} attempts"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_cve_details(cve_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
99
|
+
"""Query official CVE Services API (CVE List V5 format)."""
|
|
100
|
+
url = f"https://cveawg.mitre.org/api/cve/{cve_id}"
|
|
101
|
+
return fetch_json(url)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_epss_score(cve_id: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
105
|
+
"""Query FIRST EPSS API for exploitation probability and percentile."""
|
|
106
|
+
url = f"https://api.first.org/data/v1/epss?cve={cve_id}"
|
|
107
|
+
data, err = fetch_json(url)
|
|
108
|
+
if data and "data" in data and len(data["data"]) > 0:
|
|
109
|
+
return data["data"][0], None
|
|
110
|
+
return None, err or "No EPSS record returned"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@lru_cache(maxsize=1)
|
|
114
|
+
def get_cisa_kev_catalog() -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
|
|
115
|
+
"""
|
|
116
|
+
Downloads and caches the official CISA KEV catalog in memory.
|
|
117
|
+
Cached across lookups to avoid downloading multi-megabyte feeds repeatedly.
|
|
118
|
+
"""
|
|
119
|
+
url = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
|
120
|
+
return fetch_json(url)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def check_cisa_kev(cve_id: str) -> Dict[str, Any]:
|
|
124
|
+
"""Check if CVE is listed in CISA Known Exploited Vulnerabilities catalog."""
|
|
125
|
+
data, err = get_cisa_kev_catalog()
|
|
126
|
+
if not data:
|
|
127
|
+
return {"listed": False, "error": err or "Unable to reach CISA KEV feed"}
|
|
128
|
+
|
|
129
|
+
vulns = data.get("vulnerabilities", [])
|
|
130
|
+
target_id = cve_id.upper()
|
|
131
|
+
for v in vulns:
|
|
132
|
+
if v.get("cveID", "").upper() == target_id:
|
|
133
|
+
return {
|
|
134
|
+
"listed": True,
|
|
135
|
+
"vendorProject": v.get("vendorProject", "Unknown"),
|
|
136
|
+
"product": v.get("product", "Unknown"),
|
|
137
|
+
"vulnerabilityName": v.get("vulnerabilityName", "N/A"),
|
|
138
|
+
"dateAdded": v.get("dateAdded", "N/A"),
|
|
139
|
+
"requiredAction": v.get("requiredAction", "N/A"),
|
|
140
|
+
"knownRansomwareCampaignUse": v.get("knownRansomwareCampaignUse", "Unknown"),
|
|
141
|
+
"note": "Exploitation has been confirmed historically in the wild",
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
"listed": False,
|
|
145
|
+
"note": "No federal mandate / confirmed broad in-the-wild exploitation recorded in KEV catalog",
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def classify_references(references: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
150
|
+
"""Classifies external references into general advisories vs public exploit PoCs."""
|
|
151
|
+
exploit_urls: List[str] = []
|
|
152
|
+
other_urls: List[str] = []
|
|
153
|
+
|
|
154
|
+
for ref in references:
|
|
155
|
+
url = ref.get("url", "")
|
|
156
|
+
tags = set(ref.get("tags", []))
|
|
157
|
+
is_exploit = bool(tags & EXPLOIT_TAGS) or any(pat.search(url) for pat in EXPLOIT_URL_PATTERNS)
|
|
158
|
+
|
|
159
|
+
if is_exploit:
|
|
160
|
+
exploit_urls.append(url)
|
|
161
|
+
else:
|
|
162
|
+
other_urls.append(url)
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"total": len(references),
|
|
166
|
+
"exploit_related": len(exploit_urls),
|
|
167
|
+
"exploit_urls": exploit_urls,
|
|
168
|
+
"general_urls": other_urls[:5],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def assess_ctf_relevance(
|
|
173
|
+
cve_data: Optional[Dict[str, Any]],
|
|
174
|
+
epss_info: Optional[Dict[str, Any]],
|
|
175
|
+
kev_info: Dict[str, Any],
|
|
176
|
+
ref_info: Dict[str, Any],
|
|
177
|
+
) -> Dict[str, Any]:
|
|
178
|
+
"""
|
|
179
|
+
Computes educational CTF challenge relevance heuristic score and signals.
|
|
180
|
+
Note: Decoupled strictly from EPSS (real-world probability) and CVSS (severity).
|
|
181
|
+
"""
|
|
182
|
+
score = 0.0
|
|
183
|
+
signals: List[str] = []
|
|
184
|
+
|
|
185
|
+
# 1. Public PoC / Exploit availability (strongest CTF indicator)
|
|
186
|
+
if ref_info.get("exploit_related", 0) > 0:
|
|
187
|
+
score += 0.35
|
|
188
|
+
signals.append(f"public_exploit_poc_available ({ref_info['exploit_related']} references)")
|
|
189
|
+
|
|
190
|
+
# 2. Historical confirmed exploitation (CISA KEV)
|
|
191
|
+
if kev_info.get("listed"):
|
|
192
|
+
score += 0.25
|
|
193
|
+
signals.append("cisa_kev_listed")
|
|
194
|
+
|
|
195
|
+
# 3. CVSS metrics extraction
|
|
196
|
+
cvss_score = 0.0
|
|
197
|
+
cvss_vector = ""
|
|
198
|
+
if cve_data:
|
|
199
|
+
cna = cve_data.get("containers", {}).get("cna", {})
|
|
200
|
+
metrics = cna.get("metrics", [])
|
|
201
|
+
for m in metrics:
|
|
202
|
+
for cvss_key in ("cvssV4_0", "cvssV3_1", "cvssV3_0", "cvssV2_0"):
|
|
203
|
+
if cvss_key in m:
|
|
204
|
+
cvss = m[cvss_key]
|
|
205
|
+
try:
|
|
206
|
+
cvss_score = max(cvss_score, float(cvss.get("baseScore", 0.0)))
|
|
207
|
+
except (ValueError, TypeError):
|
|
208
|
+
pass
|
|
209
|
+
cvss_vector = cvss.get("vectorString", cvss_vector)
|
|
210
|
+
|
|
211
|
+
if cvss_score >= 7.0:
|
|
212
|
+
score += 0.20
|
|
213
|
+
signals.append(f"high_severity_cvss ({cvss_score})")
|
|
214
|
+
|
|
215
|
+
if "AV:N" in cvss_vector:
|
|
216
|
+
score += 0.15
|
|
217
|
+
signals.append("network_attack_vector (AV:N)")
|
|
218
|
+
|
|
219
|
+
if "PR:N" in cvss_vector or "PR:L" in cvss_vector:
|
|
220
|
+
score += 0.10
|
|
221
|
+
signals.append("low_or_none_privileges_required")
|
|
222
|
+
|
|
223
|
+
if "UI:N" in cvss_vector:
|
|
224
|
+
score += 0.10
|
|
225
|
+
signals.append("no_user_interaction_required")
|
|
226
|
+
|
|
227
|
+
# 4. Web / RCE / Memory corruption keywords
|
|
228
|
+
text_content = ""
|
|
229
|
+
if cve_data:
|
|
230
|
+
cna = cve_data.get("containers", {}).get("cna", {})
|
|
231
|
+
title = cna.get("title", "")
|
|
232
|
+
desc = " ".join(d.get("value", "") for d in cna.get("descriptions", []))
|
|
233
|
+
text_content = f"{title} {desc}".lower()
|
|
234
|
+
|
|
235
|
+
web_keywords = ["remote code execution", "rce", "sql injection", "command injection", "buffer overflow", "deserialization", "authentication bypass", "memory corruption"]
|
|
236
|
+
matched_kws = [kw for kw in web_keywords if kw in text_content]
|
|
237
|
+
if matched_kws:
|
|
238
|
+
score += 0.15
|
|
239
|
+
signals.append(f"challenge_vulnerability_pattern ({', '.join(matched_kws[:3])})")
|
|
240
|
+
|
|
241
|
+
normalized_score = min(1.0, round(score, 2))
|
|
242
|
+
return {
|
|
243
|
+
"score": normalized_score,
|
|
244
|
+
"signals": signals,
|
|
245
|
+
"note": "CTF relevance is an educational challenge heuristic based on public exploit availability and network reachability; it is strictly distinct from EPSS and CVSS.",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def lookup_cve_threat_intel(cve_id: str) -> Dict[str, Any]:
|
|
250
|
+
"""End-to-end vulnerability threat intelligence pipeline returning structured data."""
|
|
251
|
+
norm_cve = normalize_cve_id(cve_id)
|
|
252
|
+
errors: List[str] = []
|
|
253
|
+
|
|
254
|
+
# 1. Fetch MITRE CVE V5 Record
|
|
255
|
+
cve_data, cve_err = get_cve_details(norm_cve)
|
|
256
|
+
if cve_err:
|
|
257
|
+
errors.append(cve_err)
|
|
258
|
+
|
|
259
|
+
if not cve_data:
|
|
260
|
+
return {
|
|
261
|
+
"status": "error",
|
|
262
|
+
"cve": norm_cve,
|
|
263
|
+
"error": cve_err or f"Record for {norm_cve} not found in CVE List V5 repository.",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
cve_metadata = cve_data.get("cveMetadata", {})
|
|
267
|
+
state = cve_metadata.get("state", "UNKNOWN")
|
|
268
|
+
cna_container = cve_data.get("containers", {}).get("cna", {})
|
|
269
|
+
|
|
270
|
+
title = cna_container.get("title", "No title provided by CNA")
|
|
271
|
+
descriptions = cna_container.get("descriptions", [])
|
|
272
|
+
desc_text = "No description available."
|
|
273
|
+
for d in descriptions:
|
|
274
|
+
if d.get("lang") in ("en", "en-US", None):
|
|
275
|
+
desc_text = d.get("value", desc_text)
|
|
276
|
+
break
|
|
277
|
+
|
|
278
|
+
# 2. Extract CVSS
|
|
279
|
+
cvss_info: Dict[str, Any] = {}
|
|
280
|
+
metrics = cna_container.get("metrics", [])
|
|
281
|
+
for m in metrics:
|
|
282
|
+
for cvss_key in ("cvssV4_0", "cvssV3_1", "cvssV3_0", "cvssV2_0"):
|
|
283
|
+
if cvss_key in m:
|
|
284
|
+
c = m[cvss_key]
|
|
285
|
+
cvss_info = {
|
|
286
|
+
"version": cvss_key.replace("cvssV", "").replace("_", "."),
|
|
287
|
+
"score": c.get("baseScore", "N/A"),
|
|
288
|
+
"severity": c.get("baseSeverity", "N/A"),
|
|
289
|
+
"vector": c.get("vectorString", "N/A"),
|
|
290
|
+
}
|
|
291
|
+
break
|
|
292
|
+
if cvss_info:
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
# 3. Affected Products
|
|
296
|
+
affected_list = []
|
|
297
|
+
for aff in cna_container.get("affected", [])[:5]:
|
|
298
|
+
v_list = [f"{v.get('version', '')} ({v.get('status', '')})" for v in aff.get("versions", [])[:3]]
|
|
299
|
+
affected_list.append({
|
|
300
|
+
"vendor": aff.get("vendor", "Unknown"),
|
|
301
|
+
"product": aff.get("product", "Unknown"),
|
|
302
|
+
"versions": v_list,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
# 4. FIRST EPSS
|
|
306
|
+
epss_raw, epss_err = get_epss_score(norm_cve)
|
|
307
|
+
if epss_err:
|
|
308
|
+
errors.append(epss_err)
|
|
309
|
+
|
|
310
|
+
epss_info: Dict[str, Any] = {}
|
|
311
|
+
if epss_raw:
|
|
312
|
+
epss_prob = float(epss_raw.get("epss", 0.0))
|
|
313
|
+
epss_pct = float(epss_raw.get("percentile", 0.0))
|
|
314
|
+
epss_info = {
|
|
315
|
+
"score": epss_prob,
|
|
316
|
+
"percentile": epss_pct,
|
|
317
|
+
"note": "EPSS estimates the probability of exploitation in the wild; it is not proof of active exploitation and should not be treated as a CTF exploitability score.",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
# 5. CISA KEV
|
|
321
|
+
kev_info = check_cisa_kev(norm_cve)
|
|
322
|
+
|
|
323
|
+
# 6. References Classification
|
|
324
|
+
raw_refs = cna_container.get("references", [])
|
|
325
|
+
ref_info = classify_references(raw_refs)
|
|
326
|
+
|
|
327
|
+
# 7. CTF Relevance Assessment
|
|
328
|
+
relevance = assess_ctf_relevance(cve_data, epss_raw, kev_info, ref_info)
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
"status": "success",
|
|
332
|
+
"cve": norm_cve,
|
|
333
|
+
"state": state,
|
|
334
|
+
"cna": cve_metadata.get("assignerShortName", "Unknown"),
|
|
335
|
+
"title": title,
|
|
336
|
+
"description": desc_text.strip(),
|
|
337
|
+
"affected": affected_list,
|
|
338
|
+
"cvss": cvss_info,
|
|
339
|
+
"epss": epss_info,
|
|
340
|
+
"kev": kev_info,
|
|
341
|
+
"references": ref_info,
|
|
342
|
+
"ctf_relevance": relevance,
|
|
343
|
+
"errors": errors,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def display_human_summary(intel: Dict[str, Any]) -> None:
|
|
348
|
+
"""Renders formatted console output from structured threat intelligence data."""
|
|
349
|
+
cve_id = intel["cve"]
|
|
350
|
+
print("\n" + "=" * 70)
|
|
351
|
+
print(f"[*] VULNERABILITY THREAT INTELLIGENCE: {cve_id}")
|
|
352
|
+
print("=" * 70)
|
|
353
|
+
|
|
354
|
+
print(f"\n[+] STATUS: {intel.get('state')} | CNA: {intel.get('cna')}")
|
|
355
|
+
print(f"[+] TITLE : {intel.get('title')}\n")
|
|
356
|
+
print(f"[+] DESCRIPTION:\n {intel.get('description')}\n")
|
|
357
|
+
|
|
358
|
+
# Affected products
|
|
359
|
+
affected = intel.get("affected", [])
|
|
360
|
+
if affected:
|
|
361
|
+
print("[+] AFFECTED PRODUCTS & VERSIONS:")
|
|
362
|
+
for aff in affected:
|
|
363
|
+
ver_str = ", ".join(aff["versions"]) if aff["versions"] else "Unspecified"
|
|
364
|
+
print(f" - {aff['vendor']} / {aff['product']} -> {ver_str}")
|
|
365
|
+
print()
|
|
366
|
+
|
|
367
|
+
# Metrics
|
|
368
|
+
cvss = intel.get("cvss", {})
|
|
369
|
+
if cvss:
|
|
370
|
+
print(f"[+] CVSS BASE METRICS (v{cvss.get('version', '3.x')}):")
|
|
371
|
+
print(f" - Score: {cvss.get('score')} ({cvss.get('severity')}) | Vector: {cvss.get('vector')}\n")
|
|
372
|
+
|
|
373
|
+
# EPSS
|
|
374
|
+
epss = intel.get("epss", {})
|
|
375
|
+
print("[+] EPSS PROBABILITY (FIRST.org):")
|
|
376
|
+
if epss and "score" in epss:
|
|
377
|
+
score = epss["score"]
|
|
378
|
+
pct = epss["percentile"] * 100.0
|
|
379
|
+
print(f" - Exploitation Probability (30-day): {score:.4f} ({score * 100:.2f}%)")
|
|
380
|
+
print(f" - Percentile: {pct:.2f}%")
|
|
381
|
+
print(f" - Note: {epss.get('note')}")
|
|
382
|
+
else:
|
|
383
|
+
print(" - No EPSS data available.")
|
|
384
|
+
print()
|
|
385
|
+
|
|
386
|
+
# CISA KEV
|
|
387
|
+
kev = intel.get("kev", {})
|
|
388
|
+
print("[+] CISA KEV (KNOWN EXPLOITED VULNERABILITIES):")
|
|
389
|
+
if kev.get("listed"):
|
|
390
|
+
print(f" - [HIGH] Listed in CISA KEV: YES (exploitation has been confirmed)")
|
|
391
|
+
print(f" - Vulnerability Name: {kev.get('vulnerabilityName')}")
|
|
392
|
+
print(f" - Date Added: {kev.get('dateAdded')}")
|
|
393
|
+
print(f" - Known Ransomware Campaign Use: {kev.get('knownRansomwareCampaignUse')}")
|
|
394
|
+
elif "error" in kev:
|
|
395
|
+
print(f" - Status: {kev.get('error')}")
|
|
396
|
+
else:
|
|
397
|
+
print(f" - Listed in CISA KEV: NO ({kev.get('note')})")
|
|
398
|
+
print()
|
|
399
|
+
|
|
400
|
+
# Exploit References
|
|
401
|
+
refs = intel.get("references", {})
|
|
402
|
+
print(f"[+] REFERENCES ({refs.get('total', 0)} total, {refs.get('exploit_related', 0)} exploit-related):")
|
|
403
|
+
if refs.get("exploit_urls"):
|
|
404
|
+
print(" [!] Public PoC / Exploit References:")
|
|
405
|
+
for e_url in refs["exploit_urls"][:6]:
|
|
406
|
+
print(f" * {e_url}")
|
|
407
|
+
for g_url in refs.get("general_urls", [])[:3]:
|
|
408
|
+
print(f" - {g_url}")
|
|
409
|
+
print()
|
|
410
|
+
|
|
411
|
+
# CTF Relevance
|
|
412
|
+
ctf_rel = intel.get("ctf_relevance", {})
|
|
413
|
+
print(f"[+] CTF CHALLENGE RELEVANCE (Heuristic Score: {ctf_rel.get('score', 0.0):.2f}/1.0):")
|
|
414
|
+
for sig in ctf_rel.get("signals", []):
|
|
415
|
+
print(f" - [Signal] {sig}")
|
|
416
|
+
print(f" - Note: {ctf_rel.get('note')}")
|
|
417
|
+
print("=" * 70 + "\n")
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def main() -> None:
|
|
421
|
+
import argparse
|
|
422
|
+
parser = argparse.ArgumentParser(description="CTF-Agent Vulnerability Threat Intelligence Utility v2.1")
|
|
423
|
+
parser.add_argument("cve", help="CVE identifier (e.g. CVE-2023-44487 or 2023-44487)")
|
|
424
|
+
parser.add_argument("--json", action="store_true", help="Output machine-readable JSON structure")
|
|
425
|
+
|
|
426
|
+
args = parser.parse_args()
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
norm_cve = normalize_cve_id(args.cve)
|
|
430
|
+
except ValueError as exc:
|
|
431
|
+
if args.json:
|
|
432
|
+
print(json.dumps({"status": "error", "error": str(exc)}, indent=2))
|
|
433
|
+
else:
|
|
434
|
+
print(f"[-] Input Error: {exc}", file=sys.stderr)
|
|
435
|
+
sys.exit(1)
|
|
436
|
+
|
|
437
|
+
intel = lookup_cve_threat_intel(norm_cve)
|
|
438
|
+
|
|
439
|
+
if intel.get("status") == "error":
|
|
440
|
+
if args.json:
|
|
441
|
+
print(json.dumps(intel, indent=2))
|
|
442
|
+
else:
|
|
443
|
+
print(f"[-] Error: {intel.get('error')}", file=sys.stderr)
|
|
444
|
+
sys.exit(1)
|
|
445
|
+
|
|
446
|
+
if args.json:
|
|
447
|
+
print(json.dumps(intel, indent=2))
|
|
448
|
+
else:
|
|
449
|
+
display_human_summary(intel)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
main()
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Flag Candidate Extraction Engine (v2.1)
|
|
4
|
+
Extracts, normalizes, deduplicates, and scores flag candidates from raw text, command output, or files.
|
|
5
|
+
Supports high-visibility terminal banners for Blitz mode and JSON output for CTFd automation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import json
|
|
12
|
+
import argparse
|
|
13
|
+
from dataclasses import dataclass, asdict
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
# Ensure safe UTF-8 output on Windows consoles
|
|
17
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
18
|
+
try:
|
|
19
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
20
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class FlagCandidate:
|
|
26
|
+
value: str
|
|
27
|
+
source: str
|
|
28
|
+
location: Optional[str]
|
|
29
|
+
confidence: float
|
|
30
|
+
|
|
31
|
+
# Common competition flag patterns with baseline confidence
|
|
32
|
+
DEFAULT_FLAG_PATTERNS = [
|
|
33
|
+
(r'(?<![a-zA-Z0-9_])picoCTF\{[^\}]+\}', 0.95),
|
|
34
|
+
(r'(?<![a-zA-Z0-9_])Dice\{[^\}]+\}', 0.95),
|
|
35
|
+
(r'(?<![a-zA-Z0-9_])SEKAI\{[^\}]+\}', 0.95),
|
|
36
|
+
(r'(?<![a-zA-Z0-9_])HTB\{[^\}]+\}', 0.95),
|
|
37
|
+
(r'(?<![a-zA-Z0-9_])flagyard\{[^\}]+\}', 0.95),
|
|
38
|
+
(r'(?<![a-zA-Z0-9_])FlagY\{[^\}]+\}', 0.95),
|
|
39
|
+
(r'(?<![a-zA-Z0-9_])flag\{[a-zA-Z0-9_\-\.\!\?@#\$%\^&\*\+=\s]+\}', 0.95),
|
|
40
|
+
(r'(?<![a-zA-Z0-9_])FLAG\{[a-zA-Z0-9_\-\.\!\?@#\$%\^&\*\+=\s]+\}', 0.95),
|
|
41
|
+
(r'(?<![a-zA-Z0-9_])CTF\{[a-zA-Z0-9_\-\.\!\?@#\$%\^&\*\+=\s]+\}', 0.95),
|
|
42
|
+
(r'(?<![a-zA-Z0-9_])FLAG-[A-Za-z0-9]{16,64}', 0.85),
|
|
43
|
+
(r'(?<![a-zA-Z0-9_])[0-9a-fA-F]{32}(?![a-zA-Z0-9_])', 0.30), # Raw MD5-like candidate (weak)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def extract_flag_candidates(
|
|
47
|
+
raw_text: str,
|
|
48
|
+
source: str = "terminal",
|
|
49
|
+
location: Optional[str] = None,
|
|
50
|
+
custom_prefix: Optional[str] = None
|
|
51
|
+
) -> List[FlagCandidate]:
|
|
52
|
+
candidates = []
|
|
53
|
+
seen = set()
|
|
54
|
+
|
|
55
|
+
patterns = list(DEFAULT_FLAG_PATTERNS)
|
|
56
|
+
if custom_prefix:
|
|
57
|
+
custom_regex = rf'{re.escape(custom_prefix)}\{{[^\}}]+\}}'
|
|
58
|
+
patterns.insert(0, (custom_regex, 1.0))
|
|
59
|
+
|
|
60
|
+
for pattern, base_conf in patterns:
|
|
61
|
+
matches = re.findall(pattern, raw_text)
|
|
62
|
+
for m in matches:
|
|
63
|
+
cleaned = m.strip()
|
|
64
|
+
if cleaned not in seen:
|
|
65
|
+
seen.add(cleaned)
|
|
66
|
+
conf = base_conf
|
|
67
|
+
if location and any(k in location for k in ("flag.txt", "/flag", "environ", "response")):
|
|
68
|
+
conf = min(1.0, conf + 0.05)
|
|
69
|
+
candidates.append(FlagCandidate(value=cleaned, source=source, location=location, confidence=conf))
|
|
70
|
+
|
|
71
|
+
return sorted(candidates, key=lambda c: c.confidence, reverse=True)
|
|
72
|
+
|
|
73
|
+
def print_banner(flag: str, confidence: float, source: str):
|
|
74
|
+
"""Print high-visibility terminal banner for Blitz mode."""
|
|
75
|
+
width = max(60, len(flag) + 24)
|
|
76
|
+
border = "=" * width
|
|
77
|
+
print(f"\n{border}")
|
|
78
|
+
print(f"[+] FLAG ACQUIRED: {flag}")
|
|
79
|
+
print(f"Confidence: {confidence * 100:.0f}% | Source: {source}")
|
|
80
|
+
print(f"{border}\n")
|
|
81
|
+
|
|
82
|
+
def main():
|
|
83
|
+
parser = argparse.ArgumentParser(description="CTF Flag Extraction & Banner Generator")
|
|
84
|
+
parser.add_argument("text", nargs="*", help="Raw text string to scan for flags")
|
|
85
|
+
parser.add_argument("-f", "--file", help="Read input from a file instead of stdin/args")
|
|
86
|
+
parser.add_argument("--prefix", help="Custom flag prefix (e.g., TETCTF, DEFCON)")
|
|
87
|
+
parser.add_argument("--banner", action="store_true", help="Print high-visibility Blitz flag banner on discovery")
|
|
88
|
+
parser.add_argument("--json", action="store_true", help="Output results in JSON format")
|
|
89
|
+
parser.add_argument("--exit-code", action="store_true", help="Return exit code 0 if flag found, 1 if not")
|
|
90
|
+
parser.add_argument("--submit", type=int, metavar="CHALL_ID", help="Auto-submit top flag to CTFd challenge ID if CTF_URL and CTF_TOKEN exist")
|
|
91
|
+
|
|
92
|
+
args = parser.parse_args()
|
|
93
|
+
|
|
94
|
+
raw_text = ""
|
|
95
|
+
source_name = "cli"
|
|
96
|
+
|
|
97
|
+
if args.file:
|
|
98
|
+
try:
|
|
99
|
+
with open(args.file, "r", errors="ignore") as f:
|
|
100
|
+
raw_text = f.read()
|
|
101
|
+
source_name = f"file:{args.file}"
|
|
102
|
+
except Exception as e:
|
|
103
|
+
print(f"[-] Error reading file {args.file}: {e}", file=sys.stderr)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
elif args.text:
|
|
106
|
+
raw_text = " ".join(args.text)
|
|
107
|
+
source_name = "cli_args"
|
|
108
|
+
elif not sys.stdin.isatty():
|
|
109
|
+
raw_text = sys.stdin.read()
|
|
110
|
+
source_name = "stdin"
|
|
111
|
+
|
|
112
|
+
results = extract_flag_candidates(raw_text, source=source_name, custom_prefix=args.prefix)
|
|
113
|
+
|
|
114
|
+
if not results:
|
|
115
|
+
if args.json:
|
|
116
|
+
print(json.dumps({"success": False, "candidates": []}))
|
|
117
|
+
else:
|
|
118
|
+
print("[-] No flag candidates identified.")
|
|
119
|
+
sys.exit(1 if args.exit_code else 0)
|
|
120
|
+
|
|
121
|
+
top_candidate = results[0]
|
|
122
|
+
|
|
123
|
+
if args.banner:
|
|
124
|
+
print_banner(top_candidate.value, top_candidate.confidence, top_candidate.source)
|
|
125
|
+
|
|
126
|
+
if args.json:
|
|
127
|
+
output_data = {
|
|
128
|
+
"success": True,
|
|
129
|
+
"top_flag": top_candidate.value,
|
|
130
|
+
"candidates": [asdict(c) for c in results]
|
|
131
|
+
}
|
|
132
|
+
print(json.dumps(output_data, indent=2))
|
|
133
|
+
elif not args.banner:
|
|
134
|
+
print(f"[+] Identified {len(results)} flag candidate(s):")
|
|
135
|
+
for idx, c in enumerate(results, 1):
|
|
136
|
+
print(f" {idx}. [{c.confidence:.2f}] {c.value} (Source: {c.source})")
|
|
137
|
+
|
|
138
|
+
# Optional automated CTFd submission
|
|
139
|
+
if args.submit:
|
|
140
|
+
ctf_url = os.environ.get("CTF_URL")
|
|
141
|
+
ctf_token = os.environ.get("CTF_TOKEN")
|
|
142
|
+
if ctf_url and ctf_token:
|
|
143
|
+
try:
|
|
144
|
+
from ctfd_client import CTFdClient
|
|
145
|
+
client = CTFdClient(ctf_url, ctf_token)
|
|
146
|
+
sub_res = client.submit_flag(args.submit, top_candidate.value)
|
|
147
|
+
print(f"[*] CTFd Submission Result: {sub_res.get('status', 'unknown').upper()} - {sub_res.get('message', '')}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"[!] CTFd submission failed: {e}", file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
if args.exit_code:
|
|
152
|
+
sys.exit(0)
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|