Arrêt du cœur

ap10, Niout, ronfl3x, midugh, Gryfman, Froge4s

← Arrêt du cœur · Arrêt de la kubagerie / Arrêt sauvage, admin pur / Arrêt de l'USBDump / Sauvagerie de binaire

Arrêt de la kubagerie

En plein après-midi, le SAV de Heartbeat se fait submerger par les appels des patients inquiets, incapables de prendre ou de modifier leurs rendez-vous. Les équipes soignantes, elles aussi, sont dans l'incertitude, sans accès aux dossiers médicaux ni aux outils de communication internes. Mais que se passe-t-il ?

Tous les services Kubernetes se sont arrêtés soudainement, sans avertissement et sans raison apparente. Vous vous penchez donc sur les logs d’audit du cluster.

Cote 16 pts

Indices

Kubernetes était configuré pour recréer des ressources automatiquement en fonction de la charge. Si le cluster a subit un DoS cela signifie que des actions plus définitives ont été entreprises pour arrêter le cluster.

Faire son rapport

Même zone horaire que les logs

Analyse de logs

Pour la réalisation de cet exercice, un fichier de journaux d'audit de Kubernetes a été fourni. Il contient toutes les informations nécessaires pour identifier les actions effectuées par les utilisateurs et les applications sur le cluster.

Objectif

L'objectif de cet exercice est d'analyser les journaux d'audit pour identifier les actions malveillantes effectuées par un utilisateur spécifique.

Identification des actions malveillantes

Le scénario décrit une entreprise ayant récemment subi un déni de service. Nous nous dirigeons donc vers des actions de suppression de ressources Kubernetes, telles que les pods, les services et les déploiements. La technique que nous avons utilisée était d’analyser le nombre de delete effectués par les 4 utilisateurs par fenêtre glissante. Ensuite, il suffit d’observer quel utilisateur a un nombre de delete le plus élevé et à quel moment. Pour ce faire, étant donné que le fichier de log est extrêmement lourd, nous avons décidé de faire un script en Python. Je vous mets le script à la fin du fichier markdown.

Une fois le script lancé, nous obtenons ce résultat qu’il nous suffit d’analyser :

>>> Détection de pics DELETE par utilisateur <<<
Fenêtre: 5m | Top par utilisateur: 3
====================================================================================================

Utilisateur: REDACTED
----------------------------------------------------------------------------------------------------
[#1] REDACTED.399676+00:00  →  2025-01-20T11:57:02.527382+00:00  |  deletes=104  |  ~1511.7/min
      Top minutes    : 2025-01-20T11:57:00+00:00 (58), 2025-01-20T11:56:00+00:00 (46)
      Top namespaces : kube-system (32), redis (18), mongodb (14), vault (14), intranet (12)
      Top resources  : pods (50), services (24), deployments (18), statefulsets (8), jobs (4)
[#2] 2025-02-23T13:14:30.602858+00:00  →  2025-02-23T13:18:42.721336+00:00  |  deletes=102  |  ~24.3/min
      Top minutes    : 2025-02-23T13:18:00+00:00 (30), 2025-02-23T13:15:00+00:00 (28), 2025-02-23T13:14:00+00:00 (20), 2025-02-23T13:17:00+00:00 (20), 2025-02-23T13:16:00+00:00 (4)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)
[#3] 2025-02-21T07:14:26.372314+00:00  →  2025-02-21T07:16:34.014435+00:00  |  deletes=100  |  ~47.0/min
      Top minutes    : 2025-02-21T07:15:00+00:00 (46), 2025-02-21T07:14:00+00:00 (28), 2025-02-21T07:16:00+00:00 (26)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (100)

Utilisateur: k3s-cloud-controller-manager
----------------------------------------------------------------------------------------------------
[#1] 2025-01-20T11:57:01.238264+00:00  →  2025-01-20T11:57:01.251828+00:00  |  deletes=2  |  ~120.0/min
      Top minutes    : 2025-01-20T11:57:00+00:00 (2)
      Top namespaces : kube-system (2)
      Top resources  : daemonsets (2)

Utilisateur: martin.matin
----------------------------------------------------------------------------------------------------
[#1] 2025-02-28T23:51:47.095207+00:00  →  2025-02-28T23:54:20.469307+00:00  |  deletes=104  |  ~40.7/min
      Top minutes    : 2025-02-28T23:53:00+00:00 (42), 2025-02-28T23:52:00+00:00 (38), 2025-02-28T23:54:00+00:00 (16), 2025-02-28T23:51:00+00:00 (8)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (104)
[#2] 2025-02-26T23:43:27.882982+00:00  →  2025-02-26T23:45:50.495289+00:00  |  deletes=102  |  ~42.9/min
      Top minutes    : 2025-02-26T23:44:00+00:00 (43), 2025-02-26T23:45:00+00:00 (34), 2025-02-26T23:43:00+00:00 (25)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)
[#3] 2025-02-23T12:07:31.660359+00:00  →  2025-02-23T12:09:52.748810+00:00  |  deletes=100  |  ~42.5/min
      Top minutes    : 2025-02-23T12:09:00+00:00 (42), 2025-02-23T12:08:00+00:00 (38), 2025-02-23T12:07:00+00:00 (20)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (100)

Utilisateur: myreille.daniel
----------------------------------------------------------------------------------------------------
[#1] 2025-02-22T06:59:06.275031+00:00  →  2025-02-22T07:04:04.203386+00:00  |  deletes=110  |  ~22.2/min
      Top minutes    : 2025-02-22T07:03:00+00:00 (38), 2025-02-22T07:00:00+00:00 (34), 2025-02-22T06:59:00+00:00 (32), 2025-02-22T07:04:00+00:00 (4), 2025-02-22T07:02:00+00:00 (2)
      Top namespaces : test-myreille-daniel-5317 (8), test-daniel-daniel-1994 (6), test-martin-matin-1623 (6), test-patrick-pinson-8938 (6), test-daniel-daniel-8921 (4)
      Top resources  : deployments (110)
[#2] 2025-02-23T15:44:19.269702+00:00  →  2025-02-23T15:46:32.023144+00:00  |  deletes=102  |  ~46.1/min
      Top minutes    : 2025-02-23T15:45:00+00:00 (46), 2025-02-23T15:44:00+00:00 (34), 2025-02-23T15:46:00+00:00 (22)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)
[#3] 2025-02-03T17:22:18.960045+00:00  →  2025-02-03T17:24:52.468232+00:00  |  deletes=102  |  ~39.9/min
      Top minutes    : 2025-02-03T17:23:00+00:00 (40), 2025-02-03T17:24:00+00:00 (32), 2025-02-03T17:22:00+00:00 (30)
      Top namespaces : test-patrick-pinson-8938 (6), test-myreille-daniel-8419 (6), test-daniel-daniel-8792 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4)
      Top resources  : deployments (102)

Utilisateur: patrick.pinson
----------------------------------------------------------------------------------------------------
[#1] 2025-02-27T01:08:53.476960+00:00  →  2025-02-27T01:11:20.463412+00:00  |  deletes=102  |  ~41.6/min
      Top minutes    : 2025-02-27T01:10:00+00:00 (43), 2025-02-27T01:09:00+00:00 (38), 2025-02-27T01:11:00+00:00 (15), 2025-02-27T01:08:00+00:00 (6)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)
[#2] 2025-02-22T11:13:28.856561+00:00  →  2025-02-22T11:16:52.816945+00:00  |  deletes=102  |  ~30.0/min
      Top minutes    : 2025-02-22T11:15:00+00:00 (36), 2025-02-22T11:16:00+00:00 (36), 2025-02-22T11:13:00+00:00 (19), 2025-02-22T11:14:00+00:00 (11)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)
[#3] 2025-02-26T21:38:20.600073+00:00  →  2025-02-26T21:42:02.090110+00:00  |  deletes=102  |  ~27.6/min
      Top minutes    : 2025-02-26T21:39:00+00:00 (30), 2025-02-26T21:40:00+00:00 (26), 2025-02-26T21:41:00+00:00 (26), 2025-02-26T21:38:00+00:00 (18), 2025-02-26T21:42:00+00:00 (2)
      Top namespaces : test-patrick-pinson-8938 (6), test-daniel-daniel-1994 (4), test-myreille-daniel-5317 (4), test-martin-matin-1623 (4), test-daniel-daniel-2008 (4)
      Top resources  : deployments (102)

Nous avons décidé d'afficher un peu plus de détails nous permettant d'analyser au mieux les actions des différents utilisateurs.

Analyse des actions

En en observant l'output du script nous nous rendons compte d'une massive quantité d'actions de suppression (~1511.7/min) effectuées par l'utilisateur Daniel le 20 janvier 2025 entre 11:56:58 et 11:57:02. Cela correspond à un kubectl delete all d'où la réponse à la première question de l'exercice.

Déduction des réponses pour les autres questions

Grâce aux journaux récupérés, les réponses aux autres questions sont triviales.

Fichier python

import os
import sys
import json
from datetime import datetime, timedelta, timezone
from collections import defaultdict, Counter


# --- Paramètres fixes ---
WINDOW = timedelta(minutes=5)  # fenêtre glissante
TOP = 3                       # top pics par utilisateur


def is_system_user(username):
    if not username:
        return True
    return (
        username.startswith("system:")
        or ":serviceaccount:" in username
        or username == "kube-apiserver"
    )


def parse_ts(ts):
    if ts.endswith("Z"):
        return datetime.fromisoformat(ts.replace("Z", "+00:00"))
    if len(ts) == 10 and ts[4] == "-" and ts[7] == "-":
        return datetime.fromisoformat(ts + "T00:00:00+00:00")
    dt = datetime.fromisoformat(ts)
    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)
    return dt


def floor_minute(dt):
    return dt.replace(second=0, microsecond=0)


def sliding_windows_max(events, window):
    res = []
    i = 0
    for j, t in enumerate(events):
        while i <= j and (t - events[i]) > window:
            i += 1
        res.append((i, j, j - i + 1))
    return res


def select_top_nonoverlapping(candidates, topk):
    selected = []
    used_ranges = []
    for i, j, c in candidates:
        ok = True
        for ui, uj in used_ranges:
            ov = max(0, min(j, uj) - max(i, ui) + 1)  # overlap en nb d'index
            if ov > 0:
                if ov >= 0.5 * (j - i + 1) or ov >= 0.5 * (uj - ui + 1):
                    ok = False
                    break
        if ok:
            selected.append((i, j, c))
            used_ranges.append((i, j))
        if len(selected) >= topk:
            break
    return selected


def main():
    if len(sys.argv) != 2:
        print(f"Usage: {os.path.basename(sys.argv[0])} /chemin/vers/audit_logs.jsonl", file=sys.stderr)
        sys.exit(1)

    input_path = sys.argv[1]
    if not os.path.exists(input_path):
        print(f"[Erreur] Fichier introuvable: {input_path}", file=sys.stderr)
        sys.exit(1)

    per_user_events = defaultdict(list)

    with open(input_path, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                ev = json.loads(line)
            except json.JSONDecodeError:
                continue

            if ev.get("verb") != "delete":
                continue

            user = (ev.get("user") or {}).get("username") or "<unknown>"
            if is_system_user(user):
                continue

            ts_str = ev.get("requestReceivedTimestamp") or ev.get("stageTimestamp")
            if not ts_str:
                continue
            try:
                ts = parse_ts(ts_str)
            except Exception:
                continue

            obj = ev.get("objectRef") or {}
            ns = obj.get("namespace") or "<cluster-scope>"
            res = obj.get("resource") or obj.get("apiGroup") or "<unknown-res>"
            name = obj.get("name") or ""
            per_user_events[user].append((ts, ns, res, name))

    if not per_user_events:
        print("Aucun évènement DELETE (hors comptes système).")
        return

    for u in per_user_events:
        per_user_events[u].sort(key=lambda x: x[0])

    print("\n>>> Détection de pics DELETE par utilisateur <<<")
    print(f"Fenêtre: {int(WINDOW.total_seconds()//60)}m | Top par utilisateur: {TOP}")
    print("=" * 100)

    for user in sorted(per_user_events.keys()):
        events = per_user_events[user]
        times = [e[0] for e in events]
        if not times:
            continue

        # calcul des fenêtres glissantes
        windows = sliding_windows_max(times, WINDOW)
        windows_sorted = sorted(
            windows,
            key=lambda t: (t[2], -(times[t[1]] - times[t[0]]).total_seconds(), times[t[1]]),
            reverse=True,
        )

        top_sel = select_top_nonoverlapping(windows_sorted, TOP)

        if not top_sel:
            continue

        print(f"\nUtilisateur: {user}")
        print("-" * 100)
        for rank, (i, j, c) in enumerate(top_sel, start=1):
            t0, t1 = times[i], times[j]
            duration = max(timedelta(seconds=1), t1 - t0)  # éviter /0
            per_min = c / (duration.total_seconds() / 60.0)

            slice_ev = events[i : j + 1]
            by_min = Counter(floor_minute(ts) for ts, _, _, _ in slice_ev).most_common(5)
            top_ns = Counter(ns for _, ns, _, _ in slice_ev).most_common(5)
            top_res = Counter(res for _, _, res, _ in slice_ev).most_common(5)

            print(f"[#{rank}] {t0.isoformat()}  →  {t1.isoformat()}  |  deletes={c}  |  ~{per_min:.1f}/min")
            print("      Top minutes    : " + ", ".join(f"{m.isoformat()} ({n})" for m, n in by_min))
            print("      Top namespaces : " + ", ".join(f"{ns} ({n})" for ns, n in top_ns))
            print("      Top resources  : " + ", ".join(f"{res} ({n})" for res, n in top_res))


if __name__ == "__main__":
    main()