Skip to main content

Forensique Réseau Sans Capture de Paquets : Reconstruire le Mouvement Latéral à partir du Cache DNS, NetFlow et des Journaux d'Authentification

· 39 min read
Inference Defense
Renseignement sur les Menaces & Ingénierie de Détection

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 :

  1. 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
  2. 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
  3. L'analyse NetFlow lecture des enregistrements de flux pour détecter le scanning interne, le mouvement latéral et l'exfiltration préparée
  4. 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
  5. 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

SourceOù StockéeRétention par DéfautVolatile ?L'Attaquant Peut Détruire ?
Cache DNS clientMémoire (service DNS Client Windows)Jusqu'au redémarrage ou expiration TTLOui la plus hauteipconfig /flushdns
Journaux de requêtes serveur DNSEVTX / fichier plat sur serveur DNSDésactivé par défautMoyenEffacer le log, désactiver la journalisation
Journaux serveur DHCPC:\Windows\System32\dhcp\7 fichiers journaux quotidiensMoyenSupprimer les fichiers journaux
Base de données de baux DHCPC:\Windows\System32\dhcp\dhcp.mdbBaux actifs uniquementFaibleNécessite l'accès au serveur DHCP
Enregistrements NetFlowAppliance collecteur / SIEMSemaines à moisFaibleNécessite l'accès au collecteur
Journaux auth Windows (4624)Security.evtx / SIEMSelon taille log / SIEMMoyenEffacement du journal d'événements (1102)
Table ARP (routeur)Mémoire du routeurMinutes à heuresLa plus hauteVolatile par conception
Enregistrements DNS passifs (SIEM)SIEM si collectéSelon rétention SIEMFaibleNé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 :

IDDescriptionSignification Forensique
10AssignNouveau bail l'appareil est apparu sur le réseau à ce moment
11RenewRenouvellement de bail appareil toujours actif
12ReleaseLe client a libéré l'IP proprement arrêt normal
13DNS UpdateDHCP a enregistré l'enregistrement DNS A au nom du client
14DNS Update FailedMise à jour DNS dynamique échouée peut indiquer une manipulation DNS
15Lease ExpiredLe client s'est déconnecté sans libérer crash, déconnexion abrupte
24IP Address in UseConflit potentiellement IP statique non autorisée ou MAC usurpée
25IP Address DeletedBail supprimé manuellement par l'admin
50-59Équivalents IPv6Mê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énementEmplacement JournalCe qu'il EnregistreSignification Mouvement Latéral
4624Sécurité (hôte cible)Connexion réussieType 3 = connexion réseau ; mappe la connexion réseau à l'identité
4625Sécurité (hôte cible)Connexion échouéeForce brute, échecs pass-the-hash, scanning
4648Sécurité (hôte source)Identifiants explicites utilisésAttaquant utilisant des identifiants alternatifs depuis un hôte
4672Sécurité (hôte cible)Privilèges spéciaux attribuésAccès équivalent admin sur la cible
4769Sécurité (DC)Requête Kerberos TGSQuel ticket de service a été demandé depuis quel hôte
4776Sécurité (DC)Validation identifiants NTLMAuth NTLM inclut poste source et compte
4768Sécurité (DC)Requête Kerberos TGTAuth Kerberos initiale inclut IP source
4771Sécurité (DC)Échec pré-auth KerberosKerberos é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'AttaquantCe qui est DétruitCe qui Survit
ipconfig /flushdns sur la sourceCache DNS localJournaux de requêtes serveur DNS, enregistrements DHCP, NetFlow
wevtutil cl Security sur la cibleJournal Sécurité de la cibleEnregistrements 4769 du DC montrant le ticket de service vers la cible, NetFlow montrant la connexion
Supprimer les fichiers journaux DHCPJournaux DHCP quotidiensBase de données des baux actifs (dhcp.mdb), SIEM si les journaux ont été ingérés
Usurper l'adresse MACMAC correcte dans les journaux DHCPMAC anormale absente de l'inventaire, événements de conflit IP (DHCP ID 24)
VPN / proxy via un autre hôte interneIP source directe dans NetFlowL'hôte intermédiaire montre un nombre élevé de connexions, DHCP montre la présence de l'intermédiaire
Désactiver NetFlow sur le commutateurDonnées NetFlow futuresNetFlow historique d'avant l'événement de désactivation
Renommer l'ordinateur avant le déplacement latéralNom d'hôte dans DNSLa 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 :

QuestionSource PrincipaleSource SecondaireCommande
À quels hôtes X a-t-il parlé ?NetFlowCache DNSnfdump "src ip X and dst net internal"
Quel nom d'hôte possédait l'IP Y au moment T ?Journaux DHCPEnregistrements DNS PTRresolve_ip(Y, T) depuis dhcp_correlator.py
Quel compte a été utilisé pour la connexion ?DC Sécurité 4624/4769Cible 4624Get-WinEvent ... Id 4624 -FilterXPath
Pass-the-hash a-t-il été utilisé ?DC/Cible 4624 KeyLength=0Motif port NTLM NetFlowChamp KeyLength dans XML 4624
Quand l'attaquant est-il apparu pour la première fois ?Premier événement Assign DHCPEnregistrement le plus ancien NetFlownfdump earliest + première vue DHCP
Quels hôtes ont été scannés mais non compromis ?Flux SYN-only NetFlowCache DNS de la sourceAnalyse des drapeaux TCP dans nfdump
Quelles données ont été préparées/exfiltrées ?Octets NetFlow, grands transferts hors heuresCache DNS de l'hôte de préparationnfdump "dst net internal and byt > 100MB"
L'attaquant a-t-il modifié les journaux ?Événement 1102, 4719 sur les DCAnomalie de volume SIEMGet-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.