Advanced IP Tracing Techniques: A Technical Deep Dive for Threat Hunters
Comprehensive methodologies for tracking scammers, threat actors, and malicious infrastructure

The cat-and-mouse game between defenders and attackers has never been more complex. With threat actors leveraging cloud infrastructure, bulletproof hosting, fast-flux DNS, and rotating command-and-control (C2) servers, traditional IP tracing methods are insufficient. This comprehensive guide explores both established techniques and cutting-edge approaches for attribution and infrastructure hunting.
Github repo: https://github.com/Bodhi8/threattracer
Understanding the Attribution Challenge
IP addresses represent the most volatile element in threat intelligence. According to Datadog’s threat intelligence documentation, IP addresses often change reputations within a 24-hour cycle due to their dynamic nature in residential and mobile networks.
The 2025 IBM X-Force Threat Intelligence Index reveals critical shifts:
- 30% of intrusions now begin with valid account credentials
- 84% increase in infostealer malware delivered via phishing
- Threat actors increasingly abuse cloud hosting services for campaigns
These trends mean IP-based tracking must be combined with behavioral, cryptographic, and infrastructure-level analysis.
The Pyramid of Pain
When we use techniques that fingerprint adversary infrastructure and tools rather than easily-changed IOCs like IPs and domains, we move higher on David Bianco’s Pyramid of Pain — increasing the cost for attackers to evade detection.
/\
/ \
/ TTP \ ← Hardest to change
/--------\
/ Tools \
/------------\
/ Network/Host \
/ Artifacts \
/------------------\
/ Domain Names \
/----------------------\
/ IP Addresses \ ← Easiest to change
/--------------------------\Passive DNS Analysis
Passive DNS (pDNS) collects historical DNS resolution data without querying authoritative servers directly. This provides invaluable intelligence about domain-to-IP relationships over time.
How Passive DNS Works
Unlike active DNS queries, passive DNS sensors observe DNS traffic at recursive resolvers, collecting:
- Domain names queried
- Resolved IP addresses
- First-seen and last-seen timestamps
- Record types (A, AAAA, CNAME, MX, NS, TXT)
Key Use Cases
1. Infrastructure Pivoting
When you have a known malicious IP, passive DNS reveals all domains that have ever pointed to it:
# Example: Query passive DNS for historical resolutions # Using Farsight DNSDB API curl -H "X-API-Key: $DNSDB_API_KEY" \ "https://api.dnsdb.info/lookup/rdata/ip/192.0.2.1?limit=100"
2. Tracking Domain Migrations
Threat actors frequently rotate infrastructure. Passive DNS shows when domains moved between IPs:
example-malware.com ├── 2024-01-15: 192.0.2.1 (Hosting Provider A) ├── 2024-03-22: 198.51.100.5 (Bulletproof Host B) └── 2024-06-10: 203.0.113.42 (Cloud Provider C)
3. Identifying Shared Infrastructure
Multiple campaigns often share infrastructure. Passive DNS reveals these connections:
# Python example: Find domains sharing an IP
import requests
def find_cohosted_domains(ip_address, api_key):
"""Query passive DNS for all domains resolving to an IP"""
headers = {"X-API-Key": api_key}
url = f"https://api.passivetotal.org/v2/dns/passive?query={ip_address}"
response = requests.get(url, headers=headers)
results = response.json()
domains = set()
for record in results.get('results', []):
domains.add(record.get('resolve'))
return domains
# Usage
cohosted = find_cohosted_domains("192.0.2.1", API_KEY)
print(f"Found {len(cohosted)} domains sharing this IP")Passive DNS Tools and Services
Commercial Options:
- Farsight DNSDB — Largest passive DNS database
- PassiveTotal (RiskIQ) — Integrated with threat intel
- SecurityTrails — Historical DNS records with freemium tier
Free/Freemium Options:
- VirusTotal — Limited historical data
- DNSDumpster — Good for quick lookups
- Spamhaus Passive DNS — Focus on abuse data
Red Flags in Passive DNS Data
When analyzing passive DNS records, watch for:
- Rapid IP rotation — Multiple IPs in short timeframes suggests fast-flux
- Low TTL values — Often indicates dynamic infrastructure
- Shared nameservers — Multiple suspicious domains using same NS
- Recently registered domains — Combined with suspicious patterns
- Privacy/proxy registrations — WHOIS privacy across multiple domains
TLS Fingerprinting Deep Dive
TLS fingerprinting identifies clients and servers based on their cryptographic negotiation patterns. Because these patterns are determined by the application’s TLS implementation, they persist even when IPs and domains change.
JA3: Client Fingerprinting
JA3 creates fingerprints from the TLS Client Hello packet by extracting:
- SSL/TLS Version
- Accepted Cipher Suites
- List of Extensions
- Elliptic Curves (Supported Groups)
- Elliptic Curve Point Formats
JA3 Calculation
import hashlib
def calculate_ja3(client_hello):
"""
Calculate JA3 fingerprint from TLS Client Hello
JA3 = MD5(SSLVersion,Ciphers,Extensions,EllipticCurves,EllipticCurvePointFormats)
"""
# Extract fields from Client Hello
ssl_version = str(client_hello.version)
# Cipher suites as dash-separated decimals
ciphers = "-".join([str(c) for c in client_hello.cipher_suites])
# Extensions as dash-separated decimals (excluding GREASE values)
extensions = "-".join([str(e) for e in client_hello.extensions
if not is_grease(e)])
# Elliptic curves
curves = "-".join([str(c) for c in client_hello.supported_groups])
# EC point formats
ec_formats = "-".join([str(f) for f in client_hello.ec_point_formats])
# Build JA3 string
ja3_string = f"{ssl_version},{ciphers},{extensions},{curves},{ec_formats}"
# Calculate MD5 hash
ja3_hash = hashlib.md5(ja3_string.encode()).hexdigest()
return ja3_string, ja3_hash
def is_grease(value):
"""Check if value is a GREASE (random) value to be excluded"""
grease_values = [0x0a0a, 0x1a1a, 0x2a2a, 0x3a3a, 0x4a4a,
0x5a5a, 0x6a6a, 0x7a7a, 0x8a8a, 0x9a9a,
0xaaaa, 0xbaba, 0xcaca, 0xdada, 0xeaea, 0xfafa]
return value in grease_valuesKnown Malware JA3 Signatures
Trickbot
- JA3:
6734f37431670b3ab4292b8f60f29984 - JA3S:
623de93db17d313345d7ea481e7443cf
Emotet
- JA3:
4d7a28d6f2263ed61de88ca66eb011e3 - JA3S:
80b3a14bccc8598a1f3bbe83e71f735f
Cobalt Strike
- JA3:
72a589da586844d7f0818ce684948eea - JA3S: Various depending on configuration
Dridex
- JA3:
51c64c77e60f3980eea90869b68c58a8
IcedID
- JA3:
a0e9f5d64349fb13191bc781f81f42e1
JA3S: Server Response Fingerprinting
JA3S fingerprints how servers respond to specific clients. The key insight: C2 servers always respond to their malware client in exactly the same way.
def calculate_ja3s(server_hello):
"""
Calculate JA3S from TLS Server Hello
JA3S = MD5(SSLVersion,Cipher,Extensions)
"""
ssl_version = str(server_hello.version)
cipher = str(server_hello.cipher_suite)
extensions = "-".join([str(e) for e in server_hello.extensions])
ja3s_string = f"{ssl_version},{cipher},{extensions}"
ja3s_hash = hashlib.md5(ja3s_string.encode()).hexdigest()
return ja3s_string, ja3s_hashCombining JA3 + JA3S creates a fingerprint of the entire cryptographic negotiation, providing high-confidence identification even when infrastructure changes.
JARM: Active Server Fingerprinting
Unlike passive JA3/JA3S observation, JARM actively probes servers by sending 10 specially crafted TLS Client Hello packets and analyzing the responses.
How JARM Works
- Send 10 different TLS Client Hello packets with varying TLS versions, cipher suites, and extensions
- Capture Server Hello responses
- Extract from each response: TLS version selected, cipher suite selected, and extensions returned
- Concatenate and hash the results into a 62-character fingerprint
JARM Fingerprint Structure
The 62-byte fingerprint consists of:
- First 30 bytes: Fuzzy hash of TLS version and cipher usage
- Last 32 bytes: SHA-256 of extension usage
Calculating JARM
# Using the official JARM tool git clone https://github.com/salesforce/jarm.git cd jarm python jarm.py target.com # Output example: # Domain: target.com # JARM: 2ad2ad16d2ad2ad00042d42d00042ddb04deffa1705e2edc44cae1ed24a4da
Known C2 JARM Signatures
Cobalt Strike (default) 07d14d16d21d21d00042d41d00041de5fb3038e22f33f93c9a4ce6880e8b63
Metasploit 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1
Mythic 2ad2ad0002ad2ad00042d42d000000ad9bf51cc3f5a1e29eecb81d0c7b06eb
Sliver 2ad2ad16d2ad2ad22c42d42d00042d58c7162162b6a603d3d90a2b76865b53
Important Caveat: JARM fingerprints identify TLS implementation characteristics, not the specific application. A Cobalt Strike JARM is actually Java’s JARM fingerprint. Always correlate with additional indicators.
JA4+: Next-Generation Fingerprinting
JA4+ addresses JA3’s weaknesses (cipher reordering, GREASE values) with a modular, human-readable approach.
JA4 Format: a_b_c
t13d1516h2_8daaf6152771_b0da82dd1658
Breaking this down:
- t13d — TLS 1.3, with SNI (d=domain present)
- 15 — Number of cipher suites
- 16 — Number of extensions
- h2 — ALPN (h2=HTTP/2, h1=HTTP/1.1)
- 8daaf6152771 — Truncated SHA256 of sorted ciphers
- b0da82dd1658 — Truncated SHA256 of sorted extensions
JA4+ Suite Components
- JA4 — TLS client fingerprinting
- JA4S — TLS server fingerprinting
- JA4H — HTTP client fingerprinting
- JA4L — Light fingerprint (latency/timing)
- JA4X — X.509 certificate fingerprinting
- JA4SSH — SSH session fingerprinting
Partial Matching with JA4
JA4’s modular format enables partial matching:
# Track actor who randomizes ciphers but keeps consistent extensions
def track_by_ja4_ac(ja4_fingerprint):
"""Extract a+c parts, ignoring cipher variations"""
parts = ja4_fingerprint.split('_')
if len(parts) == 3:
return f"{parts[0]}_{parts[2]}" # a_c only
return None
# Example: GreyNoise tracks internet scanners this way
# Even with changing ciphers, the a_c fingerprint stays consistentInternet-Wide Scanning and C2 Hunting
Internet scanning platforms index billions of devices, enabling proactive threat hunting for malicious infrastructure.
Core Platforms
Shodan — Device metadata, banners, screenshots
- Example query:
http.favicon.hash:-1234567
Censys — Deep TLS/certificate analysis
- Example query:
services.jarm.fingerprint:07d14d*
FOFA — Strong Asia-Pacific coverage
- Example query:
icon_hash="-1234567"
ZoomEye — Chinese infrastructure visibility
- Example query:
iconhash:"-1234567"
Netlas — Emerging alternative platform
- Example query:
jarm:"2ad2ad16d2ad..."
Hunting Cobalt Strike Infrastructure
Cobalt Strike servers have identifiable characteristics. Here are four methods:
Method 1: Default SSL Certificate Serial
# Shodan query ssl.cert.serial:146473198 # Censys query services.tls.certificates.leaf.parsed.serial_number:"146473198"
Method 2: JARM Fingerprint
# Shodan ssl.jarm:"07d14d16d21d21d00042d41d00041de5fb3038e22f33f93c9a4ce6880e8b63" # Censys services.jarm.fingerprint:"07d14d16d21d21d00042d41d00041de5*"
Method 3: HTTP Response Characteristics
# Look for default 404 response http.html:"404 Not Found" http.headers.content-length:0 # Beacon configuration indicators http.html_hash:363333310
Method 4: Certificate Field Analysis
# Hunt for Cobalt Strike by certificate patterns
def hunt_cobalt_strike_certs(censys_api):
"""Hunt for CS servers using certificate characteristics"""
queries = [
# Default self-signed pattern
'services.tls.certificates.leaf.parsed.subject.common_name:"Major Cobalt Strike"',
# Known serial in certificate
'services.tls.certificates.leaf.parsed.serial_number:"146473198"',
# Suspicious self-signed with specific validity period
'services.tls.certificates.leaf.parsed.issuer.common_name:/^localhost$/'
]
results = []
for query in queries:
results.extend(censys_api.search(query))
return deduplicate(results)Hunting Open Directories
Threat actors often expose staging servers with open directory listings:
# Shodan queries for open directories with malicious tools
http.html:"Index of /" http.html:"mimikatz"
http.html:"Index of /" http.html:"beacon"
http.html:"Index of /" http.html:".exe" http.html:".ps1"
# Censys approach
services.http.response.body:"Index of /" AND
services.http.response.body:("mimikatz" OR "beacon.exe" OR "payload")Building Complex Queries
Combine multiple indicators for high-fidelity detection:
def build_hunt_query(indicators):
"""Build complex Shodan query from multiple indicators"""
query_parts = []
if indicators.get('jarm'):
query_parts.append(f'ssl.jarm:"{indicators["jarm"]}"')
if indicators.get('cert_serial'):
query_parts.append(f'ssl.cert.serial:{indicators["cert_serial"]}')
if indicators.get('favicon_hash'):
query_parts.append(f'http.favicon.hash:{indicators["favicon_hash"]}')
if indicators.get('html_hash'):
query_parts.append(f'http.html_hash:{indicators["html_hash"]}')
if indicators.get('asn'):
query_parts.append(f'asn:AS{indicators["asn"]}')
# Exclude known good infrastructure
if indicators.get('exclude_orgs'):
for org in indicators['exclude_orgs']:
query_parts.append(f'-org:"{org}"')
return ' '.join(query_parts)
# Example: Hunt for suspicious C2 infrastructure
query = build_hunt_query({
'jarm': '2ad2ad16d2ad2ad00042d42d00042d',
'exclude_orgs': ['Microsoft', 'Amazon', 'Google'],
})Favicon Hash Analysis
Favicons provide surprisingly effective fingerprinting because:
- Threat actors often reuse default icons
- Phishing sites clone target company favicons
- C2 frameworks ship with identifiable favicons
MurmurHash3 Calculation
Shodan uses MurmurHash3 on base64-encoded favicon data:
import mmh3
import requests
import base64
import re
def calculate_favicon_hash(url):
"""
Calculate Shodan-compatible favicon hash
Important: Shodan doesn't hash raw bytes, but a modified
base64-encoded version with specific newline formatting
"""
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
# Step 1: Base64 encode
b64_data = base64.b64encode(response.content)
# Step 2: Convert to string
b64_string = b64_data.decode('utf-8')
# Step 3: Insert newlines every 76 characters (RFC 2045)
# AND add trailing newline
with_newlines = re.sub("(.{76})", "\\1\n", b64_string, 0, re.DOTALL)
if not with_newlines.endswith('\n'):
with_newlines += '\n'
# Step 4: Calculate MurmurHash3
favicon_hash = mmh3.hash(with_newlines)
return favicon_hash
except Exception as e:
print(f"Error: {e}")
return None
# Calculate hash for a target
hash_value = calculate_favicon_hash("https://example.com/favicon.ico")
print(f"Shodan query: http.favicon.hash:{hash_value}")One-Liner Alternative
python3 -c " import mmh3,requests,codecs,sys r=requests.get(sys.argv[1]) print(mmh3.hash(codecs.encode(r.content,'base64'))) " "https://example.com/favicon.ico"
Known C2 Favicon Hashes
Cobalt Strike (default): -1018312697
- Query:
http.favicon.hash:-1018312697
Metasploit: -127886975
- Query:
http.favicon.hash:-127886975
Empire: 298876636
- Query:
http.favicon.hash:298876636
Mythic: 1982692064
- Query:
http.favicon.hash:1982692064
Hunting Phishing Infrastructure
Use favicon hashes to find phishing sites impersonating legitimate brands:
def hunt_phishing_sites(legitimate_favicon_url, excluded_orgs):
"""Find potential phishing sites using cloned favicons"""
# Calculate hash of legitimate site's favicon
target_hash = calculate_favicon_hash(legitimate_favicon_url)
# Build Shodan query
query = f'http.favicon.hash:{target_hash}'
# Exclude legitimate infrastructure
for org in excluded_orgs:
query += f' -org:"{org}"'
# Exclude hosting on legitimate domains
query += ' -hostname:*.microsoft.com'
query += ' -hostname:*.office.com'
return query
# Hunt for fake Microsoft login pages
query = hunt_phishing_sites(
"https://c.s-microsoft.com/favicon.ico",
["Microsoft Corporation", "Microsoft Azure"]
)
# Add behavioral indicators
query += ' http.html:"Sign in"' # Login page indicatorCross-Platform Searching
Different platforms use the same MurmurHash3 algorithm:
Shodan: http.favicon.hash:1234567
Censys: services.http.response.favicons.hashes.murmur3:1234567
FOFA: icon_hash="1234567"
ZoomEye: iconhash:"1234567"
SSL/TLS Certificate Intelligence
TLS certificates provide rich attribution data through fields, issuers, validity periods, and transparency logs.
Certificate Transparency (CT) Logs
CT logs are append-only, publicly auditable ledgers of all issued certificates. Over 2.5 billion certificates have been logged since 2013.
Monitoring CT Logs for Threat Hunting
# Using crt.sh API to find certificates for a domain
curl "https://crt.sh/?q=%.example.com&output=json" | jq .
# Find certificates with specific patterns
curl "https://crt.sh/?q=admin.%25&output=json" | \
jq '.[] | select(.name_value | contains("admin"))'Python CT Log Monitoring
import requests
from datetime import datetime, timedelta
def monitor_ct_logs(domain, days_back=30):
"""Monitor CT logs for certificates issued to a domain"""
url = f"https://crt.sh/?q=%.{domain}&output=json"
response = requests.get(url)
certificates = response.json()
recent = []
cutoff = datetime.now() - timedelta(days=days_back)
for cert in certificates:
entry_time = datetime.strptime(
cert['entry_timestamp'],
'%Y-%m-%dT%H:%M:%S.%f'
)
if entry_time > cutoff:
recent.append({
'id': cert['id'],
'issuer': cert['issuer_name'],
'common_name': cert['common_name'],
'name_value': cert['name_value'],
'not_before': cert['not_before'],
'not_after': cert['not_after'],
'entry_timestamp': cert['entry_timestamp']
})
return recent
# Monitor for suspicious certificates
suspicious = monitor_ct_logs("yourcompany.com")
for cert in suspicious:
print(f"New cert: {cert['common_name']} issued by {cert['issuer']}")Certificate Field Analysis
Key fields for threat hunting:
def analyze_certificate(cert_data):
"""Extract attribution-relevant certificate fields"""
analysis = {
# Subject fields
'subject_cn': cert_data.get('subject', {}).get('common_name'),
'subject_org': cert_data.get('subject', {}).get('organization'),
# Issuer fields
'issuer_cn': cert_data.get('issuer', {}).get('common_name'),
'issuer_org': cert_data.get('issuer', {}).get('organization'),
# Validity
'not_before': cert_data.get('validity', {}).get('start'),
'not_after': cert_data.get('validity', {}).get('end'),
# Fingerprints
'sha256': cert_data.get('fingerprint_sha256'),
'serial': cert_data.get('serial_number'),
# SANs (Subject Alternative Names)
'sans': cert_data.get('extensions', {}).get('subject_alt_name', {}).get('dns_names', [])
}
# Red flags
red_flags = []
# Self-signed certificate
if analysis['subject_cn'] == analysis['issuer_cn']:
red_flags.append('self_signed')
# Suspicious patterns in common name
if analysis['subject_cn'] and any(x in analysis['subject_cn'].lower()
for x in ['localhost', 'test', 'example']):
red_flags.append('suspicious_cn')
analysis['red_flags'] = red_flags
return analysisPivoting on Certificate Fields
def pivot_on_certificate(censys_client, cert_fingerprint):
"""Find related infrastructure using certificate pivots"""
pivots = {}
# Get certificate details
cert = censys_client.get_certificate(cert_fingerprint)
# Pivot 1: Same serial number
serial = cert.get('parsed', {}).get('serial_number')
if serial:
pivots['same_serial'] = censys_client.search(
f'services.tls.certificates.leaf.parsed.serial_number:"{serial}"'
)
# Pivot 2: Same issuer (skip common CAs)
issuer = cert.get('parsed', {}).get('issuer', {}).get('common_name')
if issuer and 'Let\'s Encrypt' not in issuer:
pivots['same_issuer'] = censys_client.search(
f'services.tls.certificates.leaf.parsed.issuer.common_name:"{issuer}"'
)
# Pivot 3: Same subject organization
org = cert.get('parsed', {}).get('subject', {}).get('organization')
if org:
pivots['same_org'] = censys_client.search(
f'services.tls.certificates.leaf.parsed.subject.organization:"{org}"'
)
return pivotsASN and BGP Analysis
Autonomous System Numbers (ASNs) identify networks and their relationships, providing valuable context for attribution.
Understanding ASNs for Threat Intelligence
def analyze_asn(ip_address):
"""Extract ASN information for an IP address"""
import ipwhois
obj = ipwhois.IPWhois(ip_address)
results = obj.lookup_rdap()
asn_info = {
'asn': results.get('asn'),
'asn_cidr': results.get('asn_cidr'),
'asn_country_code': results.get('asn_country_code'),
'asn_description': results.get('asn_description'),
'asn_registry': results.get('asn_registry'),
'network_name': results.get('network', {}).get('name'),
}
return asn_infoBulletproof Hosting Indicators
Certain ASNs are known for hosting malicious infrastructure. Key signals:
- High volume of abuse reports
- Lax or non-existent abuse response
- Known associations with criminal forums
- Hosting in jurisdictions with weak enforcement
- Frequent appearance in threat intel feeds
ASN Pivoting Workflow
1. Start with known malicious IP
↓
2. Identify ASN (e.g., AS12345)
↓
3. Query all prefixes announced by ASN
↓
4. Scan prefixes for similar infrastructure
↓
5. Identify related C2 servers
↓
6. Map threat actor's hosting preferencesUseful ASN Resources
Regional Internet Registries:
- ARIN (North America)
- RIPE (Europe, Middle East)
- APNIC (Asia-Pacific)
- LACNIC (Latin America)
- AFRINIC (Africa)
Lookup Tools:
- Hurricane Electric BGP Toolkit (bgp.he.net)
- PeeringDB
- BGPView
AI-Enhanced Attribution
Machine learning and graph-based analysis are transforming threat attribution.
Graph Neural Networks for Infrastructure Mapping
Research shows AI-driven correlation engines:
- Reduce false correlations by 43%
- Accelerate attribution analysis by 60%
# Conceptual example: Graph-based infrastructure analysis
import networkx as nx
def build_infrastructure_graph(iocs):
"""
Build a graph connecting related infrastructure
Nodes: IPs, domains, certificates, ASNs
Edges: Relationships (resolves_to, shares_cert, same_asn, etc.)
"""
G = nx.Graph()
for ioc in iocs:
# Add IP node
G.add_node(ioc['ip'], type='ip', asn=ioc.get('asn'))
# Add domain nodes and edges
for domain in ioc.get('domains', []):
G.add_node(domain, type='domain')
G.add_edge(ioc['ip'], domain, relationship='resolves')
# Add certificate node and edge
if ioc.get('cert_fingerprint'):
G.add_node(ioc['cert_fingerprint'], type='certificate')
G.add_edge(ioc['ip'], ioc['cert_fingerprint'],
relationship='presents_cert')
# Add JARM node
if ioc.get('jarm'):
G.add_node(ioc['jarm'], type='jarm')
G.add_edge(ioc['ip'], ioc['jarm'], relationship='jarm_match')
return G
def find_clusters(G):
"""Identify infrastructure clusters (likely same actor)"""
communities = nx.community.louvain_communities(G)
return list(communities)LLM-Assisted Threat Intelligence
Large Language Models are increasingly used for:
- Threat report summarization
- IOC extraction from unstructured text
- Pattern recognition across campaigns
- Attribution hypothesis generation
Building an Automated Hunting Pipeline
Combine all techniques into an automated workflow.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ IOC Input (IP, Domain) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Enrichment Layer │
│ Passive DNS | ASN/BGP | WHOIS | Threat Intel Feeds │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Fingerprint Collection │
│ JARM Scan | JA3/JA4 | Favicon Hash | Certificate Analysis │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Pivoting & Discovery │
│ Shodan | Censys | FOFA | CT Logs │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Correlation & Analysis │
│ Graph Analysis (NetworkX/Neo4j) | ML Clustering │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Output │
│ MISP Export | OpenCTI | STIX Bundle | Alert/Report │
└─────────────────────────────────────────────────────────────────┘Implementation Example
#!/usr/bin/env python3
"""Automated C2 Hunting Pipeline"""
import asyncio
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class InfrastructureIOC:
ip: str
domains: List[str] = None
jarm: Optional[str] = None
favicon_hash: Optional[int] = None
cert_sha256: Optional[str] = None
asn: Optional[str] = None
confidence: float = 0.0
tags: List[str] = None
class C2HuntingPipeline:
def __init__(self, config):
self.shodan_api = ShodanClient(config['shodan_key'])
self.censys_api = CensysClient(config['censys_id'], config['censys_secret'])
self.pdns_api = PassiveDNSClient(config['pdns_key'])
async def enrich_ioc(self, ip: str) -> InfrastructureIOC:
"""Full enrichment of a single IP"""
ioc = InfrastructureIOC(ip=ip)
# Parallel enrichment
tasks = [
self.get_passive_dns(ip),
self.get_jarm_fingerprint(ip),
self.get_shodan_data(ip),
self.get_censys_data(ip),
self.get_asn_info(ip)
]
results = await asyncio.gather(*tasks, return_exceptions=True)
# Merge results
pdns, jarm, shodan, censys, asn = results
ioc.domains = pdns.get('domains', []) if pdns else []
ioc.jarm = jarm
ioc.asn = asn.get('asn') if asn else None
if shodan:
ioc.favicon_hash = shodan.get('http', {}).get('favicon', {}).get('hash')
if censys:
ioc.cert_sha256 = censys.get('tls', {}).get('certificate', {}).get('fingerprint_sha256')
return ioc
async def hunt_related(self, ioc: InfrastructureIOC) -> List[InfrastructureIOC]:
"""Find related infrastructure using multiple pivots"""
related = []
# JARM pivot
if ioc.jarm:
matches = await self.shodan_api.search(f'ssl.jarm:"{ioc.jarm}"')
for match in matches:
if match['ip_str'] != ioc.ip:
related.append(InfrastructureIOC(
ip=match['ip_str'],
jarm=ioc.jarm,
tags=['jarm_pivot']
))
# Favicon pivot
if ioc.favicon_hash:
matches = await self.shodan_api.search(
f'http.favicon.hash:{ioc.favicon_hash}'
)
for match in matches:
if match['ip_str'] != ioc.ip:
related.append(InfrastructureIOC(
ip=match['ip_str'],
favicon_hash=ioc.favicon_hash,
tags=['favicon_pivot']
))
# Certificate pivot
if ioc.cert_sha256:
matches = await self.censys_api.search(
f'services.tls.certificates.leaf.fingerprint_sha256:{ioc.cert_sha256}'
)
for match in matches:
if match['ip'] != ioc.ip:
related.append(InfrastructureIOC(
ip=match['ip'],
cert_sha256=ioc.cert_sha256,
tags=['cert_pivot']
))
return related
# Usage
async def main():
config = {
'shodan_key': 'your_key',
'censys_id': 'your_id',
'censys_secret': 'your_secret',
'pdns_key': 'your_key'
}
pipeline = C2HuntingPipeline(config)
# Start from seed IP
seed_ioc = await pipeline.enrich_ioc("192.0.2.1")
print(f"Enriched: JARM={seed_ioc.jarm[:20]}... ASN={seed_ioc.asn}")
# Find related infrastructure
related = await pipeline.hunt_related(seed_ioc)
print(f"Found {len(related)} related hosts")
if __name__ == "__main__":
asyncio.run(main())Nuclei Templates for C2 Detection
Project Discovery’s Nuclei provides templated scanning:
# cobalt-strike-c2.yaml
id: cobalt-strike-c2
info:
name: Cobalt Strike C2 - Detect
author: security-researcher
severity: high
description: Detects Cobalt Strike C2 via SSL certificate
tags: ssl,c2,cobalt-strike
ssl:
- address: "{{Host}}:{{Port}}"
matchers:
- type: dsl
dsl:
- 'contains(serial,"08:BB:00:EE")'
extractors:
- type: json
json:
- ".serial"Operational Security Considerations
When conducting threat hunting, protect yourself.
Attribution Protection
Use isolated infrastructure:
- Dedicated VMs for research
- Separate network segments
- VPN/Tor for queries
Avoid alerting adversaries:
- Passive DNS over active queries
- Rate-limit scans
- Use cached data when possible
Credential hygiene:
- Unique API keys per project
- Rotate keys after investigations
- Never reuse threat intel platform credentials
Legal Boundaries
- Passive techniques (DNS, Shodan/Censys queries): Generally legal
- Active scanning: Verify authorization
- Accessing systems: Never without explicit permission
- Data handling: Follow GDPR/CCPA for PII
Documentation
Maintain detailed records for:
- Legal defensibility
- Team knowledge sharing
- Pattern recognition over time
- Incident response support
Conclusion
Modern IP tracing requires a multi-layered approach:
- Passive DNS for historical infrastructure mapping
- TLS fingerprinting (JA3/JA4/JARM) for behavioral identification
- Internet scanning (Shodan/Censys) for proactive discovery
- Certificate analysis for cryptographic attribution
- ASN/BGP analysis for network-level context
- AI/ML for pattern recognition and clustering
No single technique provides definitive attribution. The key is correlation across multiple data sources to build high-confidence assessments of threat actor infrastructure.
The most effective threat hunters combine:
- Technical depth in fingerprinting techniques
- Automation for scale
- Creativity in pivot strategies
- Operational discipline for safety
References & Further Reading
- IBM X-Force 2025 Threat Intelligence Index
- Salesforce Engineering: TLS Fingerprinting with JA3 and JA3S
- FoxIO: JA4+ Network Fingerprinting
- Censys: A Beginner’s Guide to Tracking Malware Infrastructure
- Project Discovery: C2 Server Hunting with Nuclei Templates
- Certificate Transparency: https://certificate.transparency.dev/
What IP tracing techniques have you found most effective? Share your experiences in the comments below.
About the Author
Brian Curry is a Kansas City-based Data Scientist, AI expert, and founder of Vector1 Research, where he explores the frontier of Artificial Intelligence, Causal Inference, Agentic Systems, and Economic Modeling.
His work focuses on building intelligent systems that reveal the causal structures driving performance across marketing, content, and economics. He is the creator of PyCausalSim (causal discovery through simulation), Papilon (complex system optimization), Daedalus (causal and economic analysis), and Memory-Node Encapsulation (data structures for artificial episodic memory).
Contact: brian@vector1.ai
Tags: #Cybersecurity #ThreatIntelligence #ThreatHunting #OSINT #MalwareAnalysis #C2Hunting #TLSFingerprinting #Infosec


