Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Threat actor infrastructure tracking involves monitoring and mapping adversary-controlled assets including command-and-control (C2) servers, phishing domains, exploit kit hosts, bulletproof hosting, a
.claude/skills/tracking-threat-actor-infrastructure/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✓→✓ | = Same ✓ | — | — |
Threat actor infrastructure tracking involves monitoring and mapping adversary-controlled assets including command-and-control (C2) servers, phishing domains, exploit kit hosts, bulletproof hosting, and staging servers. This skill covers using passive DNS, certificate transparency logs, Shodan/Censys scanning, WHOIS analysis, and network fingerprinting to discover, track, and pivot across threat actor infrastructure over time.
shodan, censys, requests, stix2 librariesPivoting is the technique of using one known indicator to discover related infrastructure. Starting from a known C2 IP address, analysts can pivot via: passive DNS (find domains), reverse WHOIS (find related registrations), SSL certificates (find shared certs), SSH key fingerprints, HTTP response fingerprints, JARM/JA3S hashes, and WHOIS registrant data.
Passive DNS databases record DNS query/response data observed at recursive resolvers. This allows analysts to find historical domain-to-IP mappings, discover domains hosted on a known C2 IP, and identify fast-flux or domain generation algorithm (DGA) behavior.
Certificate Transparency (CT) logs publicly record all SSL/TLS certificates issued by CAs. Monitoring CT logs reveals new certificates registered for suspicious domains, helping identify phishing sites and C2 infrastructure before they become active.
pythonimport shodan api = shodan.Shodan("YOUR_SHODAN_API_KEY") def discover_infrastructure(ip_address): """Discover services and metadata for a target IP.""" try: host = api.host(ip_address) return { "ip": host["ip_str"], "org": host.get("org", ""), "asn": host.get("asn", ""), "isp": host.get("isp", ""), "country": host.get("country_name", ""), "city": host.get("city", ""), "os": host.get("os"), "ports": host.get("ports", []), "vulns": host.get("vulns", []), "hostnames": host.get("hostnames", []), "domains": host.get("domains", []), "tags": host.get("tags", []), "services": [ { "port": svc.get("port"), "transport": svc.get("transport"), "product": svc.get("product", ""), "version": svc.get("version", ""), "ssl_cert": svc.get("ssl", {}).get("cert", {}).get("subject", {}), "jarm": svc.get("ssl", {}).get("jarm", ""), } for svc in host.get("data", []) ], } except shodan.APIError as e: print(f"[-] Shodan error: {e}") return None def search_c2_framework(framework_name): """Search Shodan for known C2 framework signatures.""" c2_queries = { "cobalt-strike": 'product:"Cobalt Strike Beacon"', "metasploit": 'product:"Metasploit"', "covenant": 'http.html:"Covenant" http.title:"Covenant"', "sliver": 'ssl.cert.subject.cn:"multiplayer" ssl.cert.issuer.cn:"operators"', "havoc": 'http.html_hash:-1472705893', } query = c2_queries.get(framework_name.lower(), framework_name) results = api.search(query, limit=100) hosts = [] for match in results.get("matches", []): hosts.append({ "ip": match["ip_str"], "port": match["port"], "org": match.get("org", ""), "country": match.get("location", {}).get("country_name", ""), "asn": match.get("asn", ""), "timestamp": match.get("timestamp", ""), }) return hosts
pythonimport requests def passive_dns_lookup(indicator, api_key, indicator_type="ip"): """Query SecurityTrails for passive DNS records.""" base_url = "https://api.securitytrails.com/v1" headers = {"APIKEY": api_key, "Accept": "application/json"} if indicator_type == "ip": url = f"{base_url}/search/list" payload = { "filter": {"ipv4": indicator} } resp = requests.post(url, json=payload, headers=headers, timeout=30) else: url = f"{base_url}/domain/{indicator}/subdomains" resp = requests.get(url, headers=headers, timeout=30) if resp.status_code == 200: return resp.json() return None def query_passive_total(indicator, user, api_key): """Query PassiveTotal for passive DNS and WHOIS data.""" base_url = "https://api.passivetotal.org/v2" auth = (user, api_key) # Passive DNS pdns_resp = requests.get( f"{base_url}/dns/passive", params={"query": indicator}, auth=auth, timeout=30, ) # WHOIS whois_resp = requests.get( f"{base_url}/whois", params={"query": indicator}, auth=auth, timeout=30, ) results = {} if pdns_resp.status_code == 200: results["passive_dns"] = pdns_resp.json().get("results", []) if whois_resp.status_code == 200: results["whois"] = whois_resp.json() return results
pythonimport requests def search_ct_logs(domain): """Search Certificate Transparency logs via crt.sh.""" resp = requests.get( f"https://crt.sh/?q=%.{domain}&output=json", timeout=30, ) if resp.status_code == 200: certs = resp.json() unique_domains = set() cert_info = [] for cert in certs: name_value = cert.get("name_value", "") for name in name_value.split("\n"): unique_domains.add(name.strip()) cert_info.append({ "id": cert.get("id"), "issuer": cert.get("issuer_name", ""), "common_name": cert.get("common_name", ""), "name_value": name_value, "not_before": cert.get("not_before", ""), "not_after": cert.get("not_after", ""), "serial_number": cert.get("serial_number", ""), }) return { "domain": domain, "total_certificates": len(certs), "unique_domains": sorted(unique_domains), "certificates": cert_info[:50], } return None def monitor_new_certs(domains, interval_hours=1): """Monitor for newly issued certificates for a list of domains.""" from datetime import datetime, timedelta cutoff = (datetime.utcnow() - timedelta(hours=interval_hours)).isoformat() new_certs = [] for domain in domains: result = search_ct_logs(domain) if result: for cert in result.get("certificates", []): if cert.get("not_before", "") > cutoff: new_certs.append({ "domain": domain, "cert": cert, }) return new_certs
pythonfrom datetime import datetime def build_infrastructure_timeline(indicators): """Build a timeline of infrastructure changes.""" timeline = [] for ind in indicators: if "passive_dns" in ind: for record in ind["passive_dns"]: timeline.append({ "timestamp": record.get("firstSeen", ""), "event": "dns_resolution", "source": record.get("resolve", ""), "target": record.get("value", ""), "record_type": record.get("recordType", ""), }) if "certificates" in ind: for cert in ind["certificates"]: timeline.append({ "timestamp": cert.get("not_before", ""), "event": "certificate_issued", "domain": cert.get("common_name", ""), "issuer": cert.get("issuer", ""), }) timeline.sort(key=lambda x: x.get("timestamp", "")) return timeline
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +17 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.