Investigation réseau — SOC
Analyse de flux réseau, détection C2, beaconing, tunneling DNS/HTTPS
Détection de Beaconing
Indicateurs caractéristiques
| Indicateur | Valeur suspecte | Explication | |---|---|---| | Régularité | Intervalle < 5% de variation | Timers programmatiques vs humains | | Jitter | Jitter artificiel faible et constant | Jitter C2 = ±10-20% du beacon interval | | Taille payload | Très petite et constante (< 200 bytes) | Heartbeat pur, pas de vrai trafic | | Durée session | Sessions très courtes et régulières | Check-in / instruction poll | | Destination | Toujours même IP/domaine externe | Pas de CDN, pas de résolution variable | | Horaires | Continue 24/7 sans pause week-end | Automatisé, pas humain |
Splunk — Détection de beaconing
# Beaconing via transaction — connexions régulières vers même destindex=network earliest=-24h dest_ip="{{EXTERNAL_IP}}"| transaction src_ip dest_ip maxpause=10m| eval interval=duration/eventcount| where eventcount > 20 AND interval < 300| stats stdev(interval) as jitter, avg(interval) as avg_interval, countby src_ip, dest_ip, dest_port| where jitter < 10| sort jitter
# Beaconing timechart — régularité des connexionsindex=network src_ip="{{SUSPICIOUS_IP}}" dest_ip="{{EXTERNAL_IP}}" earliest=-6h| timechart span=5m count as connections| stats stdev(connections) as regularity_score# Faible stdev → connexions très régulières → probable beacon
# Top destinations suspectes par régularitéindex=network earliest=-24h action=allowedNOT (dest_ip IN ("{{CDN_RANGES}}"))dest_ip!="10.*" dest_ip!="172.16.*" dest_ip!="192.168.*"| bucket _time span=1h| stats count by src_ip, dest_ip, dest_port, _time| stats stdev(count) as regularity, avg(count) as avg_hourly, dc(_time) as hours_activeby src_ip, dest_ip, dest_port| where hours_active > 12 AND regularity < 2| sort regularity
DNS Tunneling
Indicateurs
| Indicateur | Valeur suspecte | Détection | |---|---|---| | Entropie sous-domaine | > 3.5 bits/char | Calcul Shannon entropy | | Longueur label | > 40 caractères | Labels DNS max légitimes ≈ 20 | | Types de requête | TXT, NULL, CNAME inhabituels | Protocoles de tunneling | | Volume | > 100 req/min vers même domaine parent | Exfiltration de données | | Unicité | Sous-domaines tous différents | Encodage de données en subdomain | | Domaine parent | Inconnu, enregistré récemment | Nouveau domaine = IoC |
Splunk — Analyse DNS
# DNS — Volume élevé vers même domaine parent (tunneling)index=dns earliest=-2h query_type IN ("A", "TXT", "NULL", "CNAME")| rex field=query "(?:[^.]+\.)+(?P<parent_domain>[^.]+\.[^.]+)$"| statscount as total_queries,dc(query) as unique_subdomains,avg(eval(len(query))) as avg_query_lenby src_ip, parent_domain| where unique_subdomains > 50 OR avg_query_len > 50| sort -unique_subdomains
# DNS — Entropie des sous-domaines (ioc_entropy lookup ou calcul)index=dns earliest=-1h| rex field=query "^(?P<subdomain>[^.]+)\."| eval entropy=0| eval chars=split(subdomain, "")| eval entropy=mvsum(eval(-log(1/mvcount(chars))/log(2)))# Approximation — utiliser un lookup pré-calculé en prod| where len(subdomain) > 30| table _time, src_ip, query, subdomain, len(subdomain)
# DNS — Requêtes TXT suspectes (souvent utilisées pour C2)index=dns query_type=TXT earliest=-24hNOT (query IN ("*.spf*", "*.dkim*", "*.dmarc*"))| stats count by src_ip, query, query_type| sort -count
# Elastic — DNS tunneling indicatorsdns.question.type: ("TXT" or "NULL")and not dns.question.name: ("_dmarc.*" or "_domainkey.*")# Long subdomainsdns.question.name.length > 50
Frameworks C2 — Indicateurs
Cobalt Strike
# Patterns réseau Cobalt StrikeMalleable C2 profiles modifier les headers HTTP pour se fondre dans le traficDefault beacon POST /submit.php toutes lesHeaders suspects X-Malware-ID, X-Session-ID dans requtes POSTCertificats TLS auto-signés avec CN "Major Cobalt Strike User"JA3 fingerprint connu CS defaultJARM fingerprint# Splunk — Cobalt Strike beacon defaultindexproxy url"*/submit.php" methodPOST content_length < 200 earliest-6h| stats count by src_ip dest_domain content_length| where count > 10
Metasploit Meterpreter
# Patterns MeterpreterHandshake TLS suivi de trafic HTTP/HTTPS vers IP publique non CDNUser-Agent vide ou gnrique sur reverse HTTPSStage download GET /AAAA payload 4 bytes puis rponse >Port par dfaut 4444 reverse_tcp 4433 reverse_https
Détection générique C2
# HTTP/HTTPS vers IPs directes (pas de domaine) — indicateur fort C2index=proxy earliest=-24hNOT (dest_domain IN ("{{KNOWN_IP_SERVICES}}"))dest_ip!="10.*" dest_ip!="192.168.*" dest_ip!="172.16.*"| rex field=url "^https?://(?P<raw_ip>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"| where isnotnull(raw_ip)| stats count by src_ip, raw_ip, dest_port, method| sort -count
Zeek & Suricata — Analyse de logs
Zeek — conn.log
# Connexions longues suspectes (possible C2 idle connection)zeek-cut idorig_h idresp_h idresp_p proto duration orig_bytes resp_bytes< connlog | awk '$5 > 3600 && $6 < 10000' | sort -k5 -rn | head -20# Connexions vers IPs inconnues sur port 443 (TLS C2)zeek-cut idorig_h idresp_h idresp_p conn_state < connlog| awk '$3 == 443 && $1 !~ /^10\./ && $1 !~ /^192\.168\./'| sort | uniq -c | sort -rn
Zeek — dns.log
# Domaines à haute entropie dans dns.logzeek-cut query qtype_name < dnslog| awk 'length($1) > 50' | sort | uniq -c | sort -rn | head -20# Requêtes TXT non-SPF/DKIMzeek-cut query qtype_name answers < dnslog| awk '$2 == "TXT" && $1 !~ /(spf|dkim|dmarc)/' | head -30
Suricata — eve.json
# Alertes C2 dans eve.jsonjq selectevent_type"alert" |timestamp timestampsrc src_ipdest dest_ipalert alertsignaturecategory alertcategory/var/log/suricata/eve.json | grep -i "C2\|beacon\|rat\|trojan"# Stats par catégorie d'alertejq -r 'select(.event_type=="alert") | .alert.category'/var/log/suricata/eve.json | sort | uniq -c | sort -rn
Combos ports/protocoles suspects
| Protocole observé | Port attendu | Port suspect | Technique | |---|---|---|---| | HTTP | 80 | 443, 8080, 8443 | HTTP over HTTPS port | | HTTPS | 443 | 8443, 8080, 4443 | C2 sur port alternatif | | DNS | 53 | 5353, 5555, 53535 | DNS tunneling sur port non-standard | | SSH | 22 | 443, 80 | SSH over HTTP port (bypass FW) | | SMB | 445 | 135, 139 | Variation SMB legacy | | RDP | 3389 | 443, 80, 3390 | RDP over HTTPS (bypass) | | IRC | 6667 | 443, 80 | IRC-based C2 (rare mais existant) |
# Détection de protocoles sur mauvais portsindex=network earliest=-24h| eval protocol_mismatch=case(dest_port=443 AND protocol="http", "HTTP_on_HTTPS_port",dest_port=80 AND protocol="https", "HTTPS_on_HTTP_port",dest_port=53 AND protocol!="dns", "non-DNS_on_DNS_port",dest_port!=22 AND protocol="ssh", "SSH_non_standard_port",true(), null())| where isnotnull(protocol_mismatch)| stats count by src_ip, dest_ip, dest_port, protocol, protocol_mismatch
TLS — Fingerprinting JA3/JA3S
# Extraire JA3 fingerprints depuis PCAP avec zeekzeek -r capturepcap policy/protocols/ssl/ja3.zeekcat ssllog | zeek-cut ja3 ja3s server_name | sort | uniq -c | sort -rn# Vérifier JA3 contre base connue (JA3er, TLS-Fingerprint.io)curl "https://ja3er.com/json/{{JA3_HASH}}"
# Splunk — JA3 malveillants connus (nécessite lookup ja3_blacklist.csv)index=ssl earliest=-24h| lookup ja3_blacklist.csv ja3_hash OUTPUT malware_family, confidence| where isnotnull(malware_family)| table _time, src_ip, dest_ip, server_name, ja3_hash, malware_family, confidence