Forensique Réseau Sans Capture de Paquets : Reconstruire le Mouvement Latéral à partir du Cache DNS, NetFlow et des Journaux d'Authentification
L'attaquant est dans votre réseau depuis six jours. Vous n'avez pas de capture de paquets. Vous n'avez pas de sonde IDS sur le trafic est-ouest. Votre licence NDR ne couvre que le périmètre. L'EDR sur l'hôte compromis a été désactivé au deuxième jour. Ce que vous avez : les journaux de requêtes du serveur DNS, les enregistrements de baux DHCP, le NetFlow de vos commutateurs cœur de réseau, et les journaux de sécurité Windows de vos contrôleurs de domaine. C'est suffisant si vous savez exactement quoi chercher, dans quel ordre, et comment corréler des sources qui n'ont jamais été conçues pour communiquer entre elles.
La Réalité Forensique à Laquelle Font Face la Plupart des Équipes IR
La capture complète de paquets du trafic est-ouest interne est la référence en matière de forensique réseau. Elle est aussi rarement présente. L'économie ne fonctionne pas pour la plupart des organisations : capturer tout le trafic interne à 10 Gbps génère environ 75 To par jour, et les coûts de stockage, de licences et d'exploitation sont prohibitifs en dehors des plus grandes entreprises.
Ce que presque toutes les organisations possèdent souvent sans réaliser sa valeur forensique est un ensemble d'artefacts réseau indirects qui, correctement corrélés, peuvent reconstruire le mouvement latéral avec une fidélité surprenante. Ces artefacts ne sont pas conçus pour la sécurité. Ils existent pour des raisons opérationnelles : le DHCP attribue des IP, le DNS résout des noms, le NetFlow mesure la bande passante et les journaux d'authentification suivent le contrôle d'accès. Mais ensemble, ils forment un enregistrement d'activité réseau qui raconte l'histoire de quelle machine a communiqué avec quelle autre machine, quand, avec quelle identité, et avec quel volume de données.
Ce billet couvre :
- La forensique du cache DNS ce qui survit sur les endpoints actifs, ce que les serveurs DNS journalisent, et comment en extraire des indicateurs de mouvement latéral
- La corrélation des journaux DHCP le mappage IP-vers-nom-d'hôte-vers-MAC qui constitue la colonne vertébrale d'identité réseau
- L'analyse NetFlow lecture des enregistrements de flux pour détecter le scanning interne, le mouvement latéral et l'exfiltration préparée
- La corrélation des journaux d'authentification Windows mappage des événements de connexion aux événements réseau pour construire une chronologie de déplacement
- La corrélation multi-sources les opérations JOIN qui transforment quatre images incomplètes en une chronologie d'attaque complète
Chaque technique inclut des commandes exactes, des scripts et des requêtes exécutables pendant une investigation active.
Partie 1 Comprendre les Preuves Préservées par Chaque Source
Avant de plonger dans les techniques, comprenez ce que chaque source capture, combien de temps elle survit, et ce que les attaquants font pour la détruire. Cela détermine votre priorité de collecte d'éléments de preuve lors de la première heure d'IR.
Matrice de Volatilité des Preuves
| Source | Où Stockée | Rétention par Défaut | Volatile ? | L'Attaquant Peut Détruire ? |
|---|---|---|---|---|
| Cache DNS client | Mémoire (service DNS Client Windows) | Jusqu'au redémarrage ou expiration TTL | Oui la plus haute | ipconfig /flushdns |
| Journaux de requêtes serveur DNS | EVTX / fichier plat sur serveur DNS | Désactivé par défaut | Moyen | Effacer le log, désactiver la journalisation |
| Journaux serveur DHCP | C:\Windows\System32\dhcp\ | 7 fichiers journaux quotidiens | Moyen | Supprimer les fichiers journaux |
| Base de données de baux DHCP | C:\Windows\System32\dhcp\dhcp.mdb | Baux actifs uniquement | Faible | Nécessite l'accès au serveur DHCP |
| Enregistrements NetFlow | Appliance collecteur / SIEM | Semaines à mois | Faible | Nécessite l'accès au collecteur |
| Journaux auth Windows (4624) | Security.evtx / SIEM | Selon taille log / SIEM | Moyen | Effacement du journal d'événements (1102) |
| Table ARP (routeur) | Mémoire du routeur | Minutes à heures | La plus haute | Volatile par conception |
| Enregistrements DNS passifs (SIEM) | SIEM si collecté | Selon rétention SIEM | Faible | Nécessite l'accès au SIEM |
Priorité de collecte : Cache DNS → BD DHCP → Journaux auth → NetFlow. Les deux premiers expirent ou sont détruits le plus rapidement. NetFlow est généralement l'artefact le plus durable.
Partie 2 Forensique du Cache DNS
2.1 Le Cache DNS Client : Une Carte de l'Activité Récente
Chaque hôte Windows maintient un cache de résolution DNS local une table en mémoire des noms d'hôtes récemment résolus et de leurs adresses IP. Ce cache est alimenté chaque fois que l'hôte communique avec un autre hôte par nom. Pour la forensique de mouvement latéral, c'est inestimable : il enregistre les noms d'hôtes internes que la machine compromise a tenté d'atteindre, même si ces connexions ont eu lieu il y a des jours et n'ont laissé aucune autre trace.
Le cache est géré par le service DNS Client (svchost.exe hébergeant Dnscache). Il ne survit que partiellement aux redémarrages certaines entrées sont persistées dans le registre pour la pré-population au prochain démarrage.
Extraction en direct depuis un hôte actif :
:: Extraction de base toutes les entrées en cache
ipconfig /displaydns
:: Format de sortie pour une seule entrée :
:: Record Name . . . . . : DC02.corp.local
:: Record Type . . . . . : 1 <- Enregistrement A (IPv4)
:: Time To Live . . . . : 1847 <- secondes restantes avant expiration
:: Data Length . . . . . : 4
:: Section . . . . . . . : Answer
:: A (Host) Record . . . : 10.10.1.15
Extraction structurée pour analyse :
# Extraire le cache DNS en objets structurés bien plus utile que la sortie brute ipconfig
# Exécuter sur l'hôte suspect ou via Invoke-Command pour la collecte distante
$dnsCache = Get-DnsClientCache | Select-Object `
Entry, # Le nom d'hôte interrogé
RecordName, # Nom d'enregistrement DNS réel (peut différer cibles CNAME)
RecordType, # 1=A, 28=AAAA, 5=CNAME, 12=PTR, 15=MX
Status, # Success, NotExist, etc.
Section, # Answer, Authority, Additional
TimeToLive, # TTL restant en secondes
DataLength,
Data # L'adresse IP résolue
# Filtrer les plages IP internes candidats au mouvement latéral
$internalRanges = @('10\.', '172\.(1[6-9]|2\d|3[01])\.', '192\.168\.')
$lateralCandidates = $dnsCache | Where-Object {
$ip = $_.Data
$isInternal = $internalRanges | Where-Object { $ip -match $_ }
$isInternal -and $_.RecordType -eq 1 # Enregistrements A uniquement
}
$lateralCandidates | Sort-Object Entry | Format-Table -AutoSize
# Exporter pour comparaison entre plusieurs hôtes
$lateralCandidates | Export-Csv "dns_cache_$(hostname)_$(Get-Date -Format 'yyyyMMddHHmm').csv" -NoTypeInformation
Collecte distante sur tous les hôtes suspects :
# Collecte en masse du cache DNS exécuter depuis le poste IR avec droits admin
$suspectHosts = @("WORKSTATION01", "WORKSTATION02", "SERVER01")
$allCacheEntries = @()
foreach ($computer in $suspectHosts) {
try {
$entries = Invoke-Command -ComputerName $computer -ScriptBlock {
Get-DnsClientCache | Select-Object Entry, RecordType, TimeToLive, Data,
@{N='SourceHost'; E={$env:COMPUTERNAME}}
} -ErrorAction Stop
$allCacheEntries += $entries
Write-Host "[+] Collecté depuis $computer : $($entries.Count) entrées"
} catch {
Write-Warning "[-] Échec sur $computer : $_"
}
}
# Trouver les hôtes ayant interrogé la même cible interne trace de mouvement latéral
$allCacheEntries |
Where-Object { $_.Data -match '^10\.' -or $_.Data -match '^172\.' } |
Group-Object Data |
Where-Object Count -gt 1 | # IP vue dans le cache de plusieurs hôtes
ForEach-Object {
Write-Host "Cible partagée : $($_.Name)" -ForegroundColor Yellow
$_.Group | Select-Object SourceHost, Entry, TimeToLive | Format-Table
}
2.2 Ce que le Cache DNS Révèle sur les Techniques d'Attaque
Différentes techniques de mouvement latéral laissent des signatures distinctes dans le cache DNS :
La collecte BloodHound est particulièrement distinctive dans le cache DNS :
# Détecter une rafale de résolution interne massive de type BloodHound dans le cache DNS
# Indicateur clé : >50 noms d'hôtes internes uniques résolus dans un seul instantané du cache
$internalEntries = Get-DnsClientCache |
Where-Object { $_.Data -match '^10\.' -and $_.RecordType -eq 1 }
# Regrouper par sous-réseau pour voir le motif de dispersion
$subnetSpread = $internalEntries | ForEach-Object {
$ip = $_.Data
$octets = $ip.Split('.')
"$($octets[0]).$($octets[1]).$($octets[2]).0/24"
} | Group-Object | Sort-Object Count -Descending
Write-Host "Sous-réseaux uniques contactés : $($subnetSpread.Count)"
Write-Host "Hôtes internes uniques résolus : $($internalEntries.Count)"
if ($internalEntries.Count -gt 50) {
Write-Warning "INDICATEUR : Nombre élevé de résolutions de noms d'hôtes internes possible énumération AD"
}
if ($subnetSpread.Count -gt 5) {
Write-Warning "INDICATEUR : Résolutions couvrant >5 sous-réseaux possible découverte réseau"
}
2.3 Journaux de Requêtes du Serveur DNS : L'Enregistrement Persistant
Le cache client est volatile. Le journal de requêtes du serveur DNS est persistant s'il est activé. Microsoft DNS Server sur Windows Server peut journaliser toutes les requêtes DNS reçues, mais cela est désactivé par défaut et doit être activé explicitement.
Activer la journalisation de débogage DNS (Windows DNS Server) :
# Activer la journalisation analytique DNS capture toutes les requêtes
# Exécuter sur le serveur DNS (généralement un DC)
# Méthode 1 : Via le module PowerShell de gestion DNS
Set-DnsServerDiagnostics -All $true -ComputerName "DNS01.corp.local"
# Méthode 2 : Paramètres spécifiques équilibre entre détail et volume
Set-DnsServerDiagnostics `
-Queries $true ` # Journaliser toutes les requêtes entrantes
-Answers $true ` # Journaliser les réponses
-SendPackets $true ` # Journaliser les paquets envoyés
-ReceivePackets $true ` # Journaliser les paquets reçus
-LogFilePath "C:\DNSDebugLog\dns.log" `
-MaxMBFileSize 500 ` # 500 Mo avant rotation
-ComputerName "DNS01.corp.local"
# Vérifier que la journalisation est active :
Get-DnsServerDiagnostics -ComputerName "DNS01.corp.local" |
Select-Object Queries, Answers, LogFilePath, MaxMBFileSize
Format du journal de débogage DNS et analyse :
# Format brut d'une entrée du journal de débogage DNS :
# Date Heure Thread Contexte Interne(I)/Externe(E) Réponse/Envoi/Réception TypeRequête TypeEnregistrement Données
#
# Exemples d'entrées dans un scénario de mouvement latéral :
2025-11-15 02:47:33 0D4 PACKET 000000AA3F012345 UDP Rcv 10.10.5.42 6D43 R Q [8081 DR NOERROR] A (6)TARGET(4)corp(5)local(0)
2025-11-15 02:47:33 0D4 PACKET 000000AA3F012346 UDP Snd 10.10.5.42 6D43 R Q [8081 DR NOERROR] A 10.10.1.55
# Cela montre : l'hôte 10.10.5.42 a interrogé TARGET.corp.local à 02:47:33
# Le serveur DNS a répondu avec 10.10.1.55
Analyser le journal de débogage DNS pour les indicateurs de mouvement latéral :
#!/usr/bin/env python3
"""
Analyser le journal de débogage DNS Windows Server pour les indicateurs de mouvement latéral.
Recherche : clients IP internes résolvant de nombreux noms d'hôtes internes (motif de découverte),
clients résolvant des noms jamais interrogés auparavant, motifs de requêtes en rafale.
"""
import re
import sys
from collections import defaultdict
from datetime import datetime
def parse_dns_debug_log(log_path):
"""Analyser le journal de débogage DNS Windows et extraire les paires client->nom d'hôte."""
# Motif pour les lignes de requête du journal de débogage DNS
query_pattern = re.compile(
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}).*?UDP Rcv\s+([\d\.]+)\s+\w+\s+Q\s+\[\w+\s+\w+\s+\w+\]\s+(\w+)\s+(.+)'
)
queries = defaultdict(list) # ip_client -> [(horodatage, nom_hôte, type_requête)]
with open(log_path, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
m = query_pattern.search(line)
if not m:
continue
ts_str, client_ip, query_type, raw_hostname = m.groups()
# Seuls les clients internes interrogeant des noms internes nous intéressent
if not client_ip.startswith(('10.', '172.', '192.168.')):
continue
# Nettoyer l'encodage du nom d'hôte (format DNS wire dans le journal de débogage)
hostname = raw_hostname.replace('(', '').replace(')', '.').strip('.')
try:
ts = datetime.strptime(ts_str, '%Y-%m-%d %H:%M:%S')
except ValueError:
continue
queries[client_ip].append((ts, hostname, query_type))
return queries
def detect_lateral_movement_patterns(queries, internal_prefix=('10.', '172.', '192.168.')):
"""Analyser les motifs de requêtes pour les indicateurs de mouvement latéral."""
findings = []
for client_ip, query_list in queries.items():
# Trier par horodatage
query_list.sort(key=lambda x: x[0])
# Trouver les rafales : >30 noms d'hôtes internes uniques résolus en 10 minutes
internal_queries = [
(ts, host) for ts, host, qt in query_list
if any(host.endswith(s) for s in ['.corp.local', '.internal', '.lan'])
]
if len(internal_queries) < 10:
continue
# Fenêtre glissante de 10 minutes
for i, (start_ts, _) in enumerate(internal_queries):
window = [
host for ts, host in internal_queries
if 0 <= (ts - start_ts).total_seconds() <= 600 # fenêtre de 10 min
]
unique_hosts_in_window = len(set(window))
if unique_hosts_in_window > 30:
findings.append({
'client_ip': client_ip,
'indicator': 'RESOLUTION_INTERNE_MASSIVE',
'detail': f'{unique_hosts_in_window} noms d\'hôtes internes uniques dans une fenêtre de 10 minutes débutant {start_ts}',
'severity': 'HIGH'
})
break
# Détecter un timing inhabituel : requêtes hors heures (minuit - 5h)
off_hours_queries = [
(ts, host) for ts, host, qt in query_list
if 0 <= ts.hour < 5
]
if len(off_hours_queries) > 20:
findings.append({
'client_ip': client_ip,
'indicator': 'ACTIVITE_HORS_HEURES',
'detail': f'{len(off_hours_queries)} requêtes entre minuit et 5h du matin',
'severity': 'MEDIUM'
})
return findings
if __name__ == '__main__':
log_file = sys.argv[1] if len(sys.argv) > 1 else r'C:\DNSDebugLog\dns.log'
print(f"[*] Analyse du journal de débogage DNS : {log_file}")
queries = parse_dns_debug_log(log_file)
print(f"[*] Trouvé {len(queries)} adresses IP client uniques")
findings = detect_lateral_movement_patterns(queries)
if not findings:
print("[+] Aucun indicateur de mouvement latéral détecté")
else:
print(f"\n[!] {len(findings)} INDICATEURS DÉTECTÉS :\n")
for f in sorted(findings, key=lambda x: x['severity']):
print(f" [{f['severity']}] {f['client_ip']}: {f['indicator']}")
print(f" {f['detail']}\n")
Partie 3 Forensique des Journaux DHCP : La Colonne Vertébrale d'Identité IP
3.1 Pourquoi les Journaux DHCP Sont Essentiels pour l'IR Réseau
Dans toute investigation active, vous rencontrerez fréquemment des adresses IP dans NetFlow, les journaux de requêtes DNS et les journaux d'authentification sans contexte de nom d'hôte. Sans corrélation DHCP, 10.10.5.42 est sans signification. Avec les journaux DHCP, 10.10.5.42 devient LAPTOP-JSMITH avec l'adresse MAC 00:1A:2B:3C:4D:5E corrélant immédiatement à un utilisateur et un appareil spécifiques dans votre inventaire des actifs.
Les journaux DHCP sont la couche de traduction IP-vers-identité qui rend toutes les autres données forensiques réseau exploitables.
3.2 Format des Journaux du Serveur DHCP Windows
Le serveur DHCP Windows maintient des fichiers journaux rotatifs quotidiens à :
C:\Windows\System32\dhcp\DhcpSrvLog-Mon.log
C:\Windows\System32\dhcp\DhcpSrvLog-Tue.log
...
C:\Windows\System32\dhcp\DhcpSrvLog-Sun.log
Chaque ligne est au format CSV :
ID,Date,Time,Description,IP Address,Host Name,MAC Address,User Name,...
10,11/15/25,02:31:04,Assign,10.10.5.42,LAPTOP-JSMITH,00-1A-2B-3C-4D-5E,,0...
11,11/15/25,02:31:04,Renew,10.10.5.42,LAPTOP-JSMITH,00-1A-2B-3C-4D-5E,,0...
12,11/15/25,10:44:17,Release,10.10.5.42,LAPTOP-JSMITH,00-1A-2B-3C-4D-5E,,0...
Identifiants d'événements clés dans les journaux DHCP :
| ID | Description | Signification Forensique |
|---|---|---|
| 10 | Assign | Nouveau bail l'appareil est apparu sur le réseau à ce moment |
| 11 | Renew | Renouvellement de bail appareil toujours actif |
| 12 | Release | Le client a libéré l'IP proprement arrêt normal |
| 13 | DNS Update | DHCP a enregistré l'enregistrement DNS A au nom du client |
| 14 | DNS Update Failed | Mise à jour DNS dynamique échouée peut indiquer une manipulation DNS |
| 15 | Lease Expired | Le client s'est déconnecté sans libérer crash, déconnexion abrupte |
| 24 | IP Address in Use | Conflit potentiellement IP statique non autorisée ou MAC usurpée |
| 25 | IP Address Deleted | Bail supprimé manuellement par l'admin |
| 50-59 | Équivalents IPv6 | Mêmes sémantiques, adresses IPv6 |
3.3 Analyse des Journaux DHCP pour la Corrélation IP-vers-Hôte
#!/usr/bin/env python3
"""
Analyser tous les fichiers journaux du serveur DHCP Windows dans un répertoire.
Construit un mappage IP-vers-nom d'hôte temporellement conscient pour la corrélation
avec d'autres artefacts forensiques pendant la réponse aux incidents.
"""
import os
import csv
import glob
from datetime import datetime
from collections import defaultdict
DHCP_EVENT_TYPES = {
'10': 'Assign',
'11': 'Renew',
'12': 'Release',
'13': 'DNS_Update',
'14': 'DNS_Update_Failed',
'15': 'Lease_Expired',
'24': 'IP_Conflict',
'25': 'Lease_Deleted'
}
def parse_dhcp_logs(log_dir):
"""
Analyser tous les fichiers DhcpSrvLog-*.log dans le répertoire.
Retourne une liste d'événements de bail triés par horodatage.
"""
events = []
log_files = glob.glob(os.path.join(log_dir, 'DhcpSrvLog-*.log'))
for log_file in log_files:
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
for line in f:
line = line.strip()
# Ignorer les en-têtes et commentaires
if not line or line.startswith('ID') or line.startswith('Microsoft') or \
line.startswith('Start') or line.startswith('Date'):
continue
parts = line.split(',')
if len(parts) < 7:
continue
event_id = parts[0].strip()
if event_id not in DHCP_EVENT_TYPES:
continue
try:
date_str = parts[1].strip()
time_str = parts[2].strip()
timestamp = datetime.strptime(f"{date_str} {time_str}", "%m/%d/%y %H:%M:%S")
except (ValueError, IndexError):
continue
events.append({
'timestamp': timestamp,
'event_type': DHCP_EVENT_TYPES[event_id],
'ip_address': parts[4].strip(),
'hostname': parts[5].strip(),
'mac_address': parts[6].strip().replace('-', ':').upper(),
'source_file': os.path.basename(log_file)
})
return sorted(events, key=lambda x: x['timestamp'])
def build_ip_timeline(events):
"""
Construire une chronologie indiquant quel nom d'hôte détenait quelle IP à quel moment.
Essentiel pour corréler les adresses IP vues dans d'autres sources de journaux.
"""
ip_timeline = defaultdict(list) # ip -> [(heure_début, heure_fin, nom_hôte, mac)]
active_leases = {} # ip -> (heure_début, nom_hôte, mac)
for event in events:
ip = event['ip_address']
hostname = event['hostname']
mac = event['mac_address']
ts = event['timestamp']
if event['event_type'] in ('Assign',):
# Nouveau bail attribué enregistrer le début
if ip in active_leases:
# Le bail précédent s'est terminé sans libération explicite
prev_start, prev_host, prev_mac = active_leases[ip]
ip_timeline[ip].append((prev_start, ts, prev_host, prev_mac))
active_leases[ip] = (ts, hostname, mac)
elif event['event_type'] in ('Release', 'Lease_Expired'):
# Bail terminé
if ip in active_leases:
start_ts, prev_host, prev_mac = active_leases.pop(ip)
ip_timeline[ip].append((start_ts, ts, prev_host, prev_mac))
# Fermer les baux encore actifs
for ip, (start_ts, hostname, mac) in active_leases.items():
ip_timeline[ip].append((start_ts, None, hostname, mac)) # None = encore actif
return ip_timeline
def resolve_ip_at_time(ip_timeline, ip_address, query_time):
"""
Étant donné une adresse IP et un horodatage, retourner quel nom d'hôte détenait cette IP.
La fonction critique pour corréler les événements réseau aux noms d'hôtes.
"""
if ip_address not in ip_timeline:
return None
for start_ts, end_ts, hostname, mac in ip_timeline[ip_address]:
if start_ts <= query_time:
if end_ts is None or query_time <= end_ts:
return {
'hostname': hostname,
'mac': mac,
'lease_start': start_ts,
'lease_end': end_ts
}
return None
# Exemple d'utilisation pendant l'IR :
if __name__ == '__main__':
DHCP_LOG_DIR = r'C:\Windows\System32\dhcp'
print("[*] Analyse des journaux DHCP...")
events = parse_dhcp_logs(DHCP_LOG_DIR)
print(f"[*] Analysé {len(events)} événements DHCP")
ip_timeline = build_ip_timeline(events)
print(f"[*] Chronologie construite pour {len(ip_timeline)} adresses IP uniques")
# Exemple : résoudre une IP vue dans NetFlow à un moment précis
investigation_ip = '10.10.5.42'
investigation_time = datetime(2025, 11, 15, 2, 47, 33) # Depuis le journal DNS/NetFlow
result = resolve_ip_at_time(ip_timeline, investigation_ip, investigation_time)
if result:
print(f"\n[+] À {investigation_time}, {investigation_ip} était détenu par :")
print(f" Nom d'hôte : {result['hostname']}")
print(f" MAC : {result['mac']}")
print(f" Bail : {result['lease_start']} → {result['lease_end'] or 'Actif'}")
else:
print(f"[-] Aucun enregistrement DHCP pour {investigation_ip} à {investigation_time}")
print(" Possible : IP statique, appareil non autorisé, ou journaux DHCP antérieurs à l'événement")
3.4 Détecter les Appareils Non Autorisés et l'Usurpation MAC dans les Journaux DHCP
Une technique courante des attaquants consiste à amener un appareil non autorisé sur le réseau ou à usurper une adresse MAC. Les journaux DHCP exposent les deux :
# Détecter les adresses MAC vues avec plusieurs noms d'hôtes différents (réutilisation MAC ou usurpation)
$dhcpLogDir = "C:\Windows\System32\dhcp"
$assignEvents = @()
Get-ChildItem "$dhcpLogDir\DhcpSrvLog-*.log" | ForEach-Object {
Get-Content $_.FullName | Where-Object { $_ -match '^10,' } | # ID événement 10 = Assign
ForEach-Object {
$parts = $_ -split ','
if ($parts.Count -ge 7 -and $parts[6] -ne '') {
$assignEvents += [PSCustomObject]@{
Timestamp = "$($parts[1]) $($parts[2])"
IP = $parts[4]
Hostname = $parts[5]
MAC = $parts[6]
}
}
}
}
# MAC avec plusieurs noms d'hôtes = suspect
$assignEvents |
Group-Object MAC |
Where-Object { ($_.Group.Hostname | Sort-Object -Unique).Count -gt 1 } |
ForEach-Object {
$hostnames = ($_.Group.Hostname | Sort-Object -Unique) -join ', '
Write-Warning "MAC $($_.Name) vue avec plusieurs noms d'hôtes : $hostnames"
$_.Group | Sort-Object Timestamp | Select-Object Timestamp, IP, Hostname, MAC |
Format-Table -AutoSize
}
Partie 4 Analyse NetFlow : Lire le Trafic Est-Ouest Sans Sonde
4.1 Ce que Contiennent les Enregistrements NetFlow
NetFlow (le protocole original de Cisco) et ses successeurs IPFIX et sFlow enregistrent les métadonnées de connexion pas le contenu des paquets. Pour chaque flux réseau (défini comme des paquets partageant le même 5-tuple : IP source, IP destination, port source, port destination, protocole), NetFlow enregistre :
Champs d'enregistrement NetFlow v9 / IPFIX :
──────────────────────────────────────────────────────────────────
Champ Type Valeur Forensique
──────────────────────────────────────────────────────────────────
src_addr IPv4/6 Adresse IP source
dst_addr IPv4/6 Adresse IP destination
src_port uint16 Port source (éphémère pour les clients)
dst_port uint16 Port destination (identifiant de service)
protocol uint8 6=TCP, 17=UDP, 1=ICMP
flow_start datetime Début du flux
flow_end datetime Fin du flux
in_bytes uint64 Octets de src vers dst
out_bytes uint64 Octets de dst vers src (flux bidirectionnels)
tcp_flags uint8 Combinaisons SYN, ACK, RST, FIN
input_snmp uint32 Index interface routeur (entrée)
output_snmp uint32 Index interface routeur (sortie)
──────────────────────────────────────────────────────────────────
Ce que NetFlow ne contient PAS : charge utile des paquets, contenu requête/réponse, détails d'authentification ou noms de processus. Il indique qu'une connexion a eu lieu, quand, pendant combien de temps, et combien de données ont transité. Combiné aux journaux DHCP et d'authentification, c'est suffisant pour reconstruire le mouvement latéral.
4.2 Activer NetFlow sur les Plateformes Courantes
Si NetFlow n'est pas déjà configuré, l'activer de manière rétroactive vous donne une couverture future. Il ne récupère pas les données historiques.
# Cisco IOS activer NetFlow sur les interfaces du commutateur interne
ip flow-export destination 10.10.1.100 9995 ! IP et port du SIEM / collecteur de flux
ip flow-export version 9
ip flow-export source GigabitEthernet0/0
interface GigabitEthernet0/1 ! Répéter pour chaque interface interne
ip flow ingress
ip flow egress
! Vérifier :
show ip flow export
show ip cache flow
# Cisco NX-OS (commutateurs datacenter) :
feature netflow
flow record SECURITY-RECORD
match ipv4 source address
match ipv4 destination address
match transport source-port
match transport destination-port
match ip protocol
collect counter bytes
collect counter packets
collect transport tcp flags
collect timestamp sys-uptime first
collect timestamp sys-uptime last
flow exporter SIEM-EXPORT
destination 10.10.1.100
transport udp 9995
version 9
flow monitor SECURITY-MONITOR
record SECURITY-RECORD
exporter SIEM-EXPORT
cache timeout active 60
interface Ethernet1/1
ip flow monitor SECURITY-MONITOR input
ip flow monitor SECURITY-MONITOR output
4.3 Requêtes NetFlow pour la Détection du Mouvement Latéral
La plupart des entreprises stockent NetFlow dans un collecteur (SolarWinds NTA, Elastic avec Logstash, Splunk stream, open-source ntopng/nfdump). Les requêtes suivantes fonctionnent avec nfdump (analyseur NetFlow open-source en ligne de commande) :
# nfdump est installé sur la plupart des collecteurs de flux basés Linux
# Fichiers NetFlow généralement stockés dans : /var/cache/nfdump/ ou /opt/nfdump/data/
# ─── SCÉNARIO 1 : Trouver toutes les connexions DEPUIS un hôte compromis connu ───
# Remplacer 10.10.5.42 par l'IP source que vous investiguez
nfdump -R /var/cache/nfdump/2025/11/15/ \
-t "2025-11-15 00:00:00-2025-11-15 23:59:59" \
-o "fmt:%ts %te %sa %da %dp %pr %byt %pkt %flg" \
"src ip 10.10.5.42 and not dst ip 10.10.5.42" | \
sort -k4 # Trier par IP destination pour regrouper les cibles latérales
# ─── SCÉNARIO 2 : Détecter le scanning de ports interne ───
# Grand nombre de destinations uniques sur le même port = scanning
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%sa %da %dp %pr %flg" \
"src net 10.10.0.0/16 and dst net 10.10.0.0/16 and \
(dst port 445 or dst port 135 or dst port 3389 or dst port 5985)" | \
awk '{print $1" "$3}' | \ # IP source + Port destination
sort | uniq -c | sort -rn | head -30
# Sortie : comptage ip_source port_dst
# Comptages élevés sur le port 445 depuis une source unique = scanning SMB = BloodHound ou préparation latérale
# ─── SCÉNARIO 3 : Trouver les connexions SMB (port 445) entre postes de travail ───
# SMB poste-à-poste est presque jamais légitime dans les environnements modernes
# Ajuster les plages de sous-réseaux à votre réseau
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%ts %sa %da %dp %byt %flg" \
"dst port 445 and src net 10.10.0.0/24 and dst net 10.10.0.0/24" | \
grep -v "10.10.0.10" # Exclure le serveur de fichiers s'il en existe un dans ce sous-réseau
# ─── SCÉNARIO 4 : Détecter le mouvement latéral RDP ───
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%ts %te %sa %da %byt" \
"dst port 3389 and src net 10.10.0.0/16 and dst net 10.10.0.0/16" | \
awk '{ bytes=$5; src=$3; dst=$4
if (bytes > 0) print src " -> " dst " bytes=" bytes }' | \
sort | uniq -c | sort -rn
# ─── SCÉNARIO 5 : Mouvement latéral WinRM (port 5985) ───
# PowerShell remoting rarement légitime entre postes de travail
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%ts %sa %da %byt" \
"dst port 5985 and src net 10.10.0.0/16"
# ─── SCÉNARIO 6 : Préparation des données grands transferts internes ───
# Avant l'exfiltration, les attaquants préparent les données sur un seul hôte
# Rechercher des transferts inhabituellement importants VERS un seul hôte interne
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%sa %da %byt" \
"src net 10.10.0.0/16 and dst net 10.10.0.0/16" | \
awk '{bytes[$2] += $3} END {for (dst in bytes) print bytes[dst], dst}' | \
sort -rn | head -20 | \
awk '{gb=$1/1073741824; printf "%-15s a reçu %.2f Go\n", $2, gb}'
Équivalent SPL Splunk pour les organisations stockant NetFlow dans Splunk :
| tstats count, sum(bytes) as total_bytes, dc(dest_ip) as unique_dests
WHERE index=netflow earliest=-24h
BY src_ip, dest_port
| where dest_port IN (445, 135, 3389, 5985, 5986, 22)
AND src_ip LIKE "10.10.%"
| eval is_internal_src = if(match(src_ip, "^10\.10\."), 1, 0)
| where is_internal_src=1
| where unique_dests > 5 /* scanning : une source touchant de nombreuses destinations */
| eval total_GB = round(total_bytes/1073741824, 3)
| sort -unique_dests
| table src_ip, dest_port, unique_dests, count, total_GB
4.4 Lecture des Drapeaux TCP pour l'Identification des Techniques d'Attaque
Les drapeaux TCP dans les enregistrements NetFlow révèlent la nature d'une connexion sans nécessiter le contenu des paquets. C'est particulièrement utile pour distinguer le scanning des sessions réelles :
Drapeaux TCP dans NetFlow (octet hexadécimal) :
─────────────────────────────────────────────────────────────────
Drapeau Hex Signification dans NetFlow Signification Attaquant
─────────────────────────────────────────────────────────────────
SYN 0x02 Tentative de connexion Scanning : nombreux SYN sans réponse SYN-ACK
SYN-ACK 0x12 Connexion acceptée Établissement de connexion normal
RST 0x04 Connexion refusée/réinitialisée Port fermé cible non à l'écoute
FIN-ACK 0x11 Terminaison propre de session Session complète terminée
SYN-RST 0x06 SYN immédiatement suivi d'un RST Scan furtif (demi-ouvert)
PSH-ACK 0x18 Transfert de données en cours Session active avec mouvement de données
─────────────────────────────────────────────────────────────────
# Trouver les flux SYN-only (scanning connexions jamais complétées)
# Ratio élevé SYN-only vers SYN-ACK sur le trafic de scanning interne
nfdump -R /var/cache/nfdump/2025/11/15/ \
-o "fmt:%ts %sa %da %dp %flg %pkt" \
"src net 10.10.0.0/16 and dst port 445" | \
awk '
/\.S\.\.\.\./ { syn_only[$3]++ } # Drapeau SYN uniquement = sans réponse
/\.SA\.\.\.\./ { syn_ack[$3]++ } # SYN-ACK = poignée de main complétée
END {
for (dst in syn_only) {
ratio = (syn_ack[dst] > 0) ? syn_only[dst]/syn_ack[dst] : 999
if (ratio > 10) { # 10x plus de SYN que de SYN-ACK = scanning
printf "SCAN détecté vers %s: %d SYN, %d SYN-ACK, ratio=%.1f\n",
dst, syn_only[dst], syn_ack[dst], ratio
}
}
}
'
Partie 5 Corrélation des Journaux d'Authentification Windows
5.1 Les Événements d'Authentification Importants pour la Forensique Réseau
Les journaux d'événements de sécurité Windows sur les contrôleurs de domaine capturent chaque tentative d'authentification réseau dans le domaine. Ces événements constituent la couche d'identité ils vous indiquent quel compte a été utilisé pour quelle connexion réseau, depuis quelle machine source.
Les identifiants d'événements clés pour la corrélation forensique réseau :
| ID Événement | Emplacement Journal | Ce qu'il Enregistre | Signification Mouvement Latéral |
|---|---|---|---|
| 4624 | Sécurité (hôte cible) | Connexion réussie | Type 3 = connexion réseau ; mappe la connexion réseau à l'identité |
| 4625 | Sécurité (hôte cible) | Connexion échouée | Force brute, échecs pass-the-hash, scanning |
| 4648 | Sécurité (hôte source) | Identifiants explicites utilisés | Attaquant utilisant des identifiants alternatifs depuis un hôte |
| 4672 | Sécurité (hôte cible) | Privilèges spéciaux attribués | Accès équivalent admin sur la cible |
| 4769 | Sécurité (DC) | Requête Kerberos TGS | Quel ticket de service a été demandé depuis quel hôte |
| 4776 | Sécurité (DC) | Validation identifiants NTLM | Auth NTLM inclut poste source et compte |
| 4768 | Sécurité (DC) | Requête Kerberos TGT | Auth Kerberos initiale inclut IP source |
| 4771 | Sécurité (DC) | Échec pré-auth Kerberos | Kerberos échoué pulvérisation de mots de passe, énumération |
5.2 Extraire les Chaînes de Déplacement Basées sur l'Authentification
La requête la plus puissante en forensique de journaux auth Windows : trouver chaque machine sur laquelle jsmith@corp.local s'est authentifié, dans l'ordre chronologique. C'est la chaîne de mouvement latéral.
# Construire la chaîne d'authentification pour un compte spécifique sur tous les DC
# Exécuter contre votre SIEM ou directement contre les journaux Sécurité DC
$targetAccount = "jsmith"
$startTime = [DateTime]::Parse("2025-11-15 00:00:00")
$endTime = [DateTime]::Parse("2025-11-16 00:00:00")
# Interroger tous les DC pour les événements de connexion impliquant le compte
$domainControllers = (Get-ADDomainController -Filter *).Name
$authEvents = @()
foreach ($dc in $domainControllers) {
Write-Host "Interrogation de $dc..."
# Événement 4624 (connexion) et 4648 (identifiants explicites) et 4769 (Kerberos TGS)
$filter = @{
LogName = 'Security'
Id = @(4624, 4648, 4769, 4776)
StartTime = $startTime
EndTime = $endTime
}
try {
$events = Get-WinEvent -ComputerName $dc -FilterHashtable $filter `
-ErrorAction Stop
foreach ($event in $events) {
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
# Extraire les champs pertinents selon l'ID d'événement
$entry = [PSCustomObject]@{
Timestamp = $event.TimeCreated
EventID = $event.Id
DC = $dc
AccountName = ($data | Where-Object Name -eq 'TargetUserName').'#text'
AccountDomain = ($data | Where-Object Name -eq 'TargetDomainName').'#text'
SourceIP = ($data | Where-Object Name -eq 'IpAddress').'#text'
Workstation = ($data | Where-Object Name -eq 'WorkstationName').'#text'
LogonType = ($data | Where-Object Name -eq 'LogonType').'#text'
AuthPackage = ($data | Where-Object Name -eq 'AuthenticationPackageName').'#text'
ServiceName = ($data | Where-Object Name -eq 'ServiceName').'#text'
LogonID = ($data | Where-Object Name -eq 'TargetLogonId').'#text'
}
# Filtrer pour notre compte cible
if ($entry.AccountName -like "*$targetAccount*") {
$authEvents += $entry
}
}
} catch {
Write-Warning "Échec sur $dc : $_"
}
}
# Trier et afficher la chaîne de déplacement
$authEvents | Sort-Object Timestamp | Format-Table Timestamp, EventID, SourceIP, Workstation, ServiceName, LogonType, AuthPackage -AutoSize
# Exporter pour corrélation avec NetFlow et données DNS
$authEvents | Export-Csv "auth_chain_${targetAccount}.csv" -NoTypeInformation
5.3 Détecter Pass-the-Hash vs. Kerberos vs. Auth Légitime
Le champ du package d'authentification dans l'Événement 4624 révèle la technique :
Analyse des champs Événement 4624 pour l'identification de la technique de mouvement latéral :
LogonType=3 (Réseau) + AuthPackage=NTLM + Incompatibilité poste source = Pass-the-Hash
LogonType=3 (Réseau) + AuthPackage=Kerberos + Heures normales = Probablement légitime
LogonType=3 (Réseau) + AuthPackage=Kerberos + Hors heures + pas de logon type 2 préalable sur cet hôte = Suspect
LogonType=9 (NewCredentials) + AuthPackage=NTLM = Identifiants alternatifs explicites (runas /netonly ou Invoke-Command avec PSCredential)
LogonType=10 (RemoteInteractive) = Session RDP
Indicateur spécifique Pass-the-Hash dans l'Événement 4624 :
KeyLength: 0 ← Ce champ à 0 dans un logon NTLM Type 3
indique qu'aucune clé de session n'a été négociée = pass-the-hash
# Détecter pass-the-hash en trouvant les connexions NTLM Type 3 avec KeyLength=0
# et où le poste de travail ne correspond pas à l'attribution DHCP de l'IP source
$pthIndicators = Get-WinEvent -ComputerName $dc -FilterHashtable @{
LogName = 'Security'; Id = 4624; StartTime = $startTime; EndTime = $endTime
} | ForEach-Object {
$xml = [xml]$_.ToXml()
$data = $xml.Event.EventData.Data
$logonType = ($data | Where-Object Name -eq 'LogonType').'#text'
$authPkg = ($data | Where-Object Name -eq 'AuthenticationPackageName').'#text'
$keyLength = ($data | Where-Object Name -eq 'KeyLength').'#text'
$sourceIP = ($data | Where-Object Name -eq 'IpAddress').'#text'
$workstation = ($data | Where-Object Name -eq 'WorkstationName').'#text'
$account = ($data | Where-Object Name -eq 'TargetUserName').'#text'
# Indicateurs pass-the-hash :
# Connexion réseau Type 3 + NTLM + KeyLength 0
if ($logonType -eq '3' -and $authPkg -eq 'NTLM' -and $keyLength -eq '0') {
[PSCustomObject]@{
Timestamp = $_.TimeCreated
Account = $account
SourceIP = $sourceIP
Workstation = $workstation
KeyLength = $keyLength
Indicator = 'POSSIBLE_PASS_THE_HASH'
}
}
} | Where-Object { $_ -ne $null }
$pthIndicators | Sort-Object Timestamp | Format-Table -AutoSize
Partie 6 Corrélation Multi-Sources : Construire la Chronologie d'Attaque
C'est ici que l'image forensique prend forme. Chaque source raconte une histoire partielle. Le JOIN sur les quatre sources construit la chronologie complète du mouvement latéral.
6.1 Le Script de Corrélation : Jointure des Quatre Sources
#!/usr/bin/env python3
"""
Moteur de corrélation multi-sources pour la forensique réseau.
Jointure de NetFlow, DHCP, cache DNS et journaux auth Windows
pour reconstruire les chronologies de mouvement latéral.
Fichiers d'entrée :
- netflow.csv: ts, src_ip, dst_ip, dst_port, bytes, flags
- dhcp.csv: timestamp, event_type, ip, hostname, mac
- auth.csv: timestamp, event_id, source_ip, account, logon_type, auth_pkg, key_length
- dns_cache.csv: source_host, resolved_hostname, resolved_ip, ttl
Sortie :
Événements de mouvement latéral avec enrichissement complet du contexte.
"""
import csv
import json
from datetime import datetime, timedelta
from collections import defaultdict
class NetworkForensicsCorrelator:
def __init__(self):
self.ip_timeline = {} # ip -> [(début, fin, nom_hôte, mac)]
self.auth_events = [] # liste des enregistrements auth
self.netflow_events = [] # liste des enregistrements de flux
self.dns_observations = {} # hôte_source -> [ips_résolues]
def load_dhcp(self, csv_path):
"""Charger et construire la chronologie IP depuis les journaux DHCP."""
events = []
with open(csv_path) as f:
for row in csv.DictReader(f):
try:
events.append({
'ts': datetime.fromisoformat(row['timestamp']),
'type': row['event_type'],
'ip': row['ip'],
'hostname': row['hostname'],
'mac': row['mac']
})
except (ValueError, KeyError):
continue
# Construire la chronologie (simplifié)
active = {}
timeline = defaultdict(list)
for ev in sorted(events, key=lambda x: x['ts']):
ip = ev['ip']
if ev['type'] == 'Assign':
if ip in active:
old = active[ip]
timeline[ip].append((old['ts'], ev['ts'], old['hostname'], old['mac']))
active[ip] = ev
elif ev['type'] in ('Release', 'Lease_Expired') and ip in active:
old = active.pop(ip)
timeline[ip].append((old['ts'], ev['ts'], old['hostname'], old['mac']))
for ip, ev in active.items():
timeline[ip].append((ev['ts'], None, ev['hostname'], ev['mac']))
self.ip_timeline = dict(timeline)
print(f"[+] DHCP : chronologie chargée pour {len(self.ip_timeline)} IPs")
def resolve_ip(self, ip, query_time):
"""Résoudre une adresse IP en nom d'hôte à un moment donné via la chronologie DHCP."""
for start, end, hostname, mac in self.ip_timeline.get(ip, []):
if start <= query_time and (end is None or query_time <= end):
return hostname, mac
return ip, 'unknown' # Repli sur IP si pas d'enregistrement DHCP
def load_netflow(self, csv_path, lateral_ports=None):
"""Charger les enregistrements NetFlow, en se concentrant sur les ports de mouvement latéral."""
if lateral_ports is None:
lateral_ports = {445, 135, 3389, 5985, 5986, 22, 23, 139}
with open(csv_path) as f:
for row in csv.DictReader(f):
try:
dst_port = int(row.get('dst_port', 0))
if dst_port not in lateral_ports:
continue
self.netflow_events.append({
'ts': datetime.fromisoformat(row['ts']),
'src_ip': row['src_ip'],
'dst_ip': row['dst_ip'],
'dst_port': dst_port,
'bytes': int(row.get('bytes', 0)),
'flags': row.get('flags', '')
})
except (ValueError, KeyError):
continue
print(f"[+] NetFlow : chargé {len(self.netflow_events)} enregistrements ports mouvement latéral")
def load_auth_logs(self, csv_path):
"""Charger les événements d'authentification Windows."""
with open(csv_path) as f:
for row in csv.DictReader(f):
try:
self.auth_events.append({
'ts': datetime.fromisoformat(row['timestamp']),
'event_id': int(row['event_id']),
'source_ip': row['source_ip'],
'account': row['account'],
'logon_type': row.get('logon_type', ''),
'auth_pkg': row.get('auth_pkg', ''),
'key_length': row.get('key_length', ''),
'service': row.get('service', '')
})
except (ValueError, KeyError):
continue
print(f"[+] Auth : chargé {len(self.auth_events)} événements d'authentification")
def correlate(self, time_window_seconds=30):
"""
Corrélation principale : pour chaque enregistrement NetFlow de mouvement latéral,
trouver l'événement auth correspondant dans la fenêtre temporelle.
Enrichir les deux avec la résolution de nom d'hôte DHCP.
"""
timeline = []
for flow in sorted(self.netflow_events, key=lambda x: x['ts']):
flow_ts = flow['ts']
src_ip = flow['src_ip']
dst_ip = flow['dst_ip']
# Résoudre les IPs en noms d'hôtes via la chronologie DHCP
src_host, src_mac = self.resolve_ip(src_ip, flow_ts)
dst_host, dst_mac = self.resolve_ip(dst_ip, flow_ts)
# Trouver l'événement auth correspondant dans la fenêtre temporelle
matching_auth = None
window = timedelta(seconds=time_window_seconds)
for auth in self.auth_events:
if abs((auth['ts'] - flow_ts).total_seconds()) <= time_window_seconds:
if auth['source_ip'] == src_ip and auth['logon_type'] in ('3', '10'):
matching_auth = auth
break
# Déterminer si c'est suspect
suspicion_flags = []
if matching_auth:
# Indicateur pass-the-hash
if (matching_auth['auth_pkg'] == 'NTLM' and
matching_auth['key_length'] == '0'):
suspicion_flags.append('PASS_THE_HASH')
# Activité hors heures (minuit à 5h)
if 0 <= flow_ts.hour < 5:
suspicion_flags.append('HORS_HEURES')
# SMB poste-à-poste (pas de motif de nom serveur)
if (flow['dst_port'] == 445 and
'srv' not in dst_host.lower() and
'server' not in dst_host.lower() and
'dc' not in dst_host.lower()):
suspicion_flags.append('SMB_POSTE_A_POSTE')
event = {
'timestamp': flow_ts.isoformat(),
'src_ip': src_ip,
'src_hostname': src_host,
'src_mac': src_mac,
'dst_ip': dst_ip,
'dst_hostname': dst_host,
'dst_mac': dst_mac,
'dst_port': flow['dst_port'],
'protocol': 'TCP',
'bytes_transferred': flow['bytes'],
'tcp_flags': flow['flags'],
'auth_account': matching_auth['account'] if matching_auth else 'INCONNU',
'auth_type': matching_auth['auth_pkg'] if matching_auth else 'INCONNU',
'logon_type': matching_auth['logon_type'] if matching_auth else 'INCONNU',
'suspicion_flags': suspicion_flags,
'severity': 'HIGH' if suspicion_flags else 'INFO'
}
timeline.append(event)
return sorted(timeline, key=lambda x: x['timestamp'])
def print_timeline(self, timeline, high_only=True):
"""Afficher une chronologie d'attaque lisible."""
print("\n" + "="*80)
print("CHRONOLOGIE DE MOUVEMENT LATÉRAL")
print("="*80)
port_names = {445: 'SMB', 135: 'RPC', 3389: 'RDP', 5985: 'WinRM', 22: 'SSH'}
for event in timeline:
if high_only and event['severity'] != 'HIGH':
continue
port_str = port_names.get(event['dst_port'], str(event['dst_port']))
mb = round(event['bytes_transferred'] / 1048576, 2)
flags_str = ', '.join(event['suspicion_flags']) if event['suspicion_flags'] else 'aucun'
print(f"\n[{event['timestamp']}] {event['severity']}")
print(f" DÉPLACEMENT : {event['src_hostname']} ({event['src_ip']})")
print(f" --> {event['dst_hostname']} ({event['dst_ip']}) via {port_str}")
print(f" IDENTITÉ : {event['auth_account']} [{event['auth_type']}, Type {event['logon_type']}]")
print(f" VOLUME : {mb} Mo transférés")
print(f" INDICATEURS: {flags_str}")
# Utilisation pendant la réponse aux incidents :
if __name__ == '__main__':
correlator = NetworkForensicsCorrelator()
correlator.load_dhcp('dhcp_export.csv')
correlator.load_netflow('netflow_internal.csv')
correlator.load_auth_logs('dc_auth_events.csv')
timeline = correlator.correlate(time_window_seconds=60)
correlator.print_timeline(timeline, high_only=True)
# Exporter la chronologie complète pour ingestion SIEM ou rapport
with open('lateral_movement_timeline.json', 'w') as f:
json.dump(timeline, f, indent=2, default=str)
print(f"\n[*] Chronologie complète exportée vers lateral_movement_timeline.json")
print(f"[*] Total événements : {len(timeline)}")
print(f"[*] Sévérité HIGH : {sum(1 for e in timeline if e['severity'] == 'HIGH')}")
Partie 7 Le Flux d'Investigation : Un Arbre de Décision pour les Équipes IR
7.1 Référence Rapide : Commandes IR par Phase
Phase 1 Collecte des Preuves (30 premières minutes)
# Sur l'hôte source suspect collecter avant redémarrage ou arrêt
ipconfig /displaydns > dns_cache_$(hostname).txt
Get-DnsClientCache | Export-Csv dns_cache_structured_$(hostname).csv -NoTypeInformation
# Sur le serveur DHCP exporter les baux actuels et les journaux
Copy-Item "C:\Windows\System32\dhcp\DhcpSrvLog-*.log" "C:\IR\dhcp_logs\"
Get-DhcpServerv4Lease -ScopeId 10.10.0.0 -AllLeases |
Select-Object IPAddress, ClientId, HostName, AddressState, LeaseExpiryTime |
Export-Csv "C:\IR\dhcp_active_leases.csv" -NoTypeInformation
# Sur les contrôleurs de domaine exporter les événements auth pour la fenêtre d'investigation
$filter = @{LogName='Security'; Id=@(4624,4625,4648,4769,4776,4768,4771,4672);
StartTime=(Get-Date).AddDays(-7); EndTime=(Get-Date)}
Get-WinEvent -FilterHashtable $filter |
ForEach-Object { $_.ToXml() } |
Out-File "C:\IR\dc_auth_events_raw.xml"
Phase 2 Requêtes NetFlow (2 premières heures)
# Sur le collecteur NetFlow identifier toutes les connexions est-ouest depuis l'hôte suspect
# Remplacer 10.10.5.42 par l'IP de votre hôte compromis
SUSPECT="10.10.5.42"
START="2025-11-14 00:00:00"
END="2025-11-15 23:59:59"
nfdump -R /var/cache/nfdump/ -t "${START}-${END}" \
-o "fmt:%ts,%te,%sa,%da,%dp,%pr,%byt,%pkt,%flg" \
"src ip ${SUSPECT} and dst net 10.0.0.0/8" > suspected_host_flows.csv
# Trouver toutes les destinations internes uniques
awk -F',' 'NR>1 {print $4}' suspected_host_flows.csv | sort -u > unique_destinations.txt
echo "Cibles internes uniques : $(wc -l < unique_destinations.txt)"
Phase 3 Corrélation et Chronologie (heures 2-4)
# Recherche IP rapide contre les journaux DHCP (utilisation en ligne de commande)
python3 dhcp_correlator.py \
--dhcp-dir /path/to/dhcp/logs \
--ip 10.10.5.42 \
--time "2025-11-15 02:47:33"
# Sortie : 10.10.5.42 à 2025-11-15 02:47:33 était LAPTOP-JSMITH (MAC: 00:1A:2B:...)
Partie 8 Les Artefacts qui Survivent au Nettoyage de l'Attaquant
Les attaquants sophistiqués tentent de supprimer les preuves. Comprendre ce qui survit au nettoyage détermine si votre investigation peut se poursuivre après que l'attaquant a tenté d'effacer ses traces.
| Action de l'Attaquant | Ce qui est Détruit | Ce qui Survit |
|---|---|---|
ipconfig /flushdns sur la source | Cache DNS local | Journaux de requêtes serveur DNS, enregistrements DHCP, NetFlow |
wevtutil cl Security sur la cible | Journal Sécurité de la cible | Enregistrements 4769 du DC montrant le ticket de service vers la cible, NetFlow montrant la connexion |
| Supprimer les fichiers journaux DHCP | Journaux DHCP quotidiens | Base de données des baux actifs (dhcp.mdb), SIEM si les journaux ont été ingérés |
| Usurper l'adresse MAC | MAC correcte dans les journaux DHCP | MAC anormale absente de l'inventaire, événements de conflit IP (DHCP ID 24) |
| VPN / proxy via un autre hôte interne | IP source directe dans NetFlow | L'hôte intermédiaire montre un nombre élevé de connexions, DHCP montre la présence de l'intermédiaire |
| Désactiver NetFlow sur le commutateur | Données NetFlow futures | NetFlow historique d'avant l'événement de désactivation |
| Renommer l'ordinateur avant le déplacement latéral | Nom d'hôte dans DNS | La corrélation par adresse MAC reste possible, anciens enregistrements DNS PTR |
L'artefact le plus résistant : NetFlow du commutateur cœur
L'attaquant aurait besoin d'un accès administratif à votre infrastructure de commutation cœur pour détruire rétroactivement NetFlow. Dans la plupart des organisations, c'est un domaine administratif distinct des serveurs Windows. Même si l'attaquant nettoie tous les journaux Windows, les enregistrements de flux montrant les connexions restent sur le collecteur.
Le deuxième plus résistant : journaux de requêtes du serveur DNS (si activé)
Le nettoyage par l'attaquant des journaux sur les postes de travail et serveurs n'affecte pas les journaux de requêtes DNS sur le serveur DNS. Ceux-ci sont particulièrement précieux car ils capturent chaque nom d'hôte que l'attaquant a résolu, y compris la reconnaissance contre des hôtes auxquels il n'a jamais réussi à se connecter.
Résumé : La Matrice de Corrélation
Quand vous avez un scénario IR et devez savoir quelles sources répondent à quelles questions, utilisez cette référence :
| Question | Source Principale | Source Secondaire | Commande |
|---|---|---|---|
| À quels hôtes X a-t-il parlé ? | NetFlow | Cache DNS | nfdump "src ip X and dst net internal" |
| Quel nom d'hôte possédait l'IP Y au moment T ? | Journaux DHCP | Enregistrements DNS PTR | resolve_ip(Y, T) depuis dhcp_correlator.py |
| Quel compte a été utilisé pour la connexion ? | DC Sécurité 4624/4769 | Cible 4624 | Get-WinEvent ... Id 4624 -FilterXPath |
| Pass-the-hash a-t-il été utilisé ? | DC/Cible 4624 KeyLength=0 | Motif port NTLM NetFlow | Champ KeyLength dans XML 4624 |
| Quand l'attaquant est-il apparu pour la première fois ? | Premier événement Assign DHCP | Enregistrement le plus ancien NetFlow | nfdump earliest + première vue DHCP |
| Quels hôtes ont été scannés mais non compromis ? | Flux SYN-only NetFlow | Cache DNS de la source | Analyse des drapeaux TCP dans nfdump |
| Quelles données ont été préparées/exfiltrées ? | Octets NetFlow, grands transferts hors heures | Cache DNS de l'hôte de préparation | nfdump "dst net internal and byt > 100MB" |
| L'attaquant a-t-il modifié les journaux ? | Événement 1102, 4719 sur les DC | Anomalie de volume SIEM | Get-WinEvent ... Id 1102, 4719 |
Références
- Microsoft Docs : ID d'événements journaux serveur DHCP référence complète des ID d'événements
- Documentation nfdump : nfdump.sourceforge.io syntaxe de requête complète
- NSA : "Detect and Prevent Web Shell Malware" méthodologie d'analyse NetFlow
- SANS : Matériaux de cours "Network Forensics Analysis" techniques d'analyse de flux
- MITRE ATT&CK T1021.002 (SMB/Windows Admin Shares) documentation du mouvement latéral
- MITRE ATT&CK T1550.002 (Pass the Hash) guide de détection
- Cisco : Guide de configuration NetFlow activation de NetFlow sur l'infrastructure Cisco
- Microsoft Security : "Token Theft Playbook" méthodologie de corrélation des journaux auth
Toutes les commandes et techniques décrites dans ce billet sont des procédures standard de réponse aux incidents et d'analyse forensique. Elles opèrent sur l'infrastructure et les journaux auxquels l'analyste a un accès administratif dans le cadre d'une investigation autorisée.