
Dans la première partie, nous avons vu comment faire le setup technique pour aborder une analyse. Cette fois, nous allons dérouler l’investigation avec une question un peu plus sérieuse qu’elle n’en a l’air : est-ce qu’un attaquant a réussi à voler le secret de la fameuse sauce Szechuan ?
Vous pouvez retrouver le fichier PCAP sur le site de DFIR Madness. Merci à l’auteur pour cet excellent jeu de données. La seule chose que nous savoir pour l’instant est que la recette de la sauce Szechuan était stocké sur un partage sur l’Active Directory.
Au départ, nous ne savons presque rien du réseau sur lequel nous opérons. Nous allons donc avancer au fur et à mesure en construisant des hypothèses, en les testant.
Compte rendu de l’investigation
Commençons par voir tout ce que nous allons découvrir dans l’investigation suivante.
Voici la chronologie de l’attaque observée dans les logs le 19 septembre 2020 :
- 02:19:26 UTC : l’IP publique
194.61.24.102semble effectuer un scan Nmap sur notre serveur Active Directory (10.42.85.10) - 02:21:26 UTC : l’IP publique
194.61.24.102réalise un grand nombre de connexions vers notre Active Directory, probablement dans le cadre d’une tentative de brute force - 02:23:41 UTC : le contrôleur de domaine télécharge en HTTP un logiciel nommé
coreupdater.exe, qui semble a priori malveillant - 02:35:55 UTC : l’attaquant présumé pivote vers une workstation Windows en utilisant le protocole RDP
- 02:36:26 UTC : une requête liée à l’extraction de secrets DPAPI est observée entre la workstation et l’Active Directory
Après cela, nous n’observons pas d’autre activité franchement malveillante dans la capture.
Nous pouvons synthétiser les actions de l’attaquant ainsi :

IOC observés
Les IOC (Indicators of Compromise) sont des marqueurs techniques qui pourront servir lors de futures investigations. Un analyste doit toujours les capitaliser pendant un incident.
- adresse IP de l’attaquant présumé :
194.61.24.102 - hash MD5 du malware :
eed41b4500e473f97c50c7385ef5e374 - adresse IP de command & control du malware :
203.78.103.109
Le mapping MITRE ATT&CK sera proposé en conclusion, une fois que nous aurons déroulé toute la chaîne.
Analyse
Comme nous l’avons vu dans l’article précédent, nos données sont maintenant extraites par Zeek et chargées dans JupyterLab.
Analyse de notre réseau
Nous commençons par établir quelques statistiques sur les connexions réseau. Le premier objectif est simple : identifier qui parle à qui, et surtout qui parle beaucoup trop.
Voyons quelles IP parlent le plus :
conn["id.orig_h"].value_counts().head(100)
id.orig_h
194.61.24.102 29313
10.42.85.115 2539
10.42.85.10 737
fe80::2dcf:e660:be73:d220 116
fe80::a926:8915:319b:2238 18
:: 3
Voyons maintenant les connexions les plus volumineuses par IP et port de destination :
conn["total_bytes"] = conn.get("orig_bytes", 0) + conn.get("resp_bytes", 0)
top_flows_bytes = (
conn[["id.orig_h", "id.resp_h", "id.resp_p", "proto", "service", "total_bytes", "orig_bytes", "resp_bytes", "duration"]]
.sort_values("total_bytes", ascending=False)
.head(5)
)
top_flows_bytes
| id.orig_h | id.resp_h | id.resp_p | proto | service | total_bytes | orig_bytes | resp_bytes | duration | |
|---|---|---|---|---|---|---|---|---|---|
| 31709 | 10.42.85.115 | 104.119.185.124 | 443 | tcp | ssl | 37991129.0 | 1858.0 | 37989271.0 | 109.659907 |
| 32078 | 194.61.24.102 | 10.42.85.10 | 3389 | tcp | ssl | 12853439.0 | 2366216.0 | 10487223.0 | 1810.715048 |
| 32089 | 10.42.85.10 | 10.42.85.115 | 3389 | udp | rdpeudp | 6448171.0 | 73592.0 | 6374579.0 | 950.971623 |
| 1698 | 10.42.85.115 | 104.18.12.165 | 443 | tcp | ssl | 4664235.0 | 2328.0 | 4661907.0 | 126.184417 |
| 1340 | 10.42.85.115 | 23.47.193.50 | 443 | tcp | ssl | 4596703.0 | 4575.0 | 4592128.0 | 33.567848 |
Ces statistiques suggèrent déjà la présence de deux IP internes principales :
10.42.85.11510.42.85.10
Nous allons maintenant tracer les flux entre ces deux IP pour tenter d’identifier leur rôle respectif.
# Filtrage par IP
internal_ips = [
"10.42.85.115",
"10.42.85.10"
]
filtered = conn[
conn["id.orig_h"].isin(internal_ips) &
conn["id.resp_h"].isin(internal_ips)
][["id.orig_h", "id.resp_h", "id.resp_p"]]
# Agrégation par IP source, IP de destination et port de destination
agg = (
filtered.groupby(["id.orig_h", "id.resp_h", "id.resp_p"])
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
agg.to_html()
| id.orig_h | id.resp_h | id.resp_p | count | |
|---|---|---|---|---|
| 1 | 10.42.85.115 | 10.42.85.10 | 53 | 656 |
| 5 | 10.42.85.115 | 10.42.85.10 | 389 | 128 |
| 2 | 10.42.85.115 | 10.42.85.10 | 88 | 42 |
| 7 | 10.42.85.115 | 10.42.85.10 | 49155 | 40 |
| 4 | 10.42.85.115 | 10.42.85.10 | 135 | 40 |
D’après les données précédentes, il semble que :
10.42.85.115soit une workstation10.42.85.10soit le contrôleur de domaine
La raison est simple : 10.42.85.115 envoie des requêtes vers 10.42.85.10 sur le port 88 (Kerberos), ce qui colle bien avec un poste de travail qui s’authentifie auprès d’un Active Directory.
Accès initial
Maintenant que nous avons identifié le rôle probable des machines, nous pouvons attaquer la première vraie question : comment l’intrusion commence-t-elle ?
Hypothèse 1 : un RAT (Remote Access Trojan) a été implanté dans le parc. Nous pourrions avoir un malware installé sur l’une de nos deux machines, qui communiquerait avec une IP externe pour recevoir ses ordres.
Nous allons commencer par regarder les IP externes les plus contactées par nos IP internes :
filtered = conn[
conn["id.orig_h"].isin(internal_ips) &
~conn["id.resp_h"].isin(internal_ips)
]
agg = (
filtered.groupby(["id.orig_h", "id.resp_h", "id.resp_p"])
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
agg[:10]
| id.orig_h | id.resp_h | id.resp_p | count | |
|---|---|---|---|---|
| 2 | 10.42.85.10 | 192.168.45.1 | 53 | 584 |
| 18 | 10.42.85.115 | 10.90.90.90 | 443 | 354 |
| 152 | 10.42.85.115 | 208.80.153.240 | 443 | 72 |
| 8 | 10.42.85.10 | 224.0.0.252 | 5355 | 67 |
| 146 | 10.42.85.115 | 204.79.197.203 | 443 | 47 |
| 258 | 10.42.85.115 | 72.21.91.29 | 80 | 46 |
| 151 | 10.42.85.115 | 208.80.153.224 | 443 | 45 |
| 213 | 10.42.85.115 | 40.91.125.0 | 443 | 40 |
| 144 | 10.42.85.115 | 204.79.197.200 | 443 | 39 |
| 254 | 10.42.85.115 | 70.37.74.6 | 443 | 38 |
Parmi les IP externes les plus contactées, nous trouvons donc :
192.168.45.1: probablement le résolveur DNS local, car contacté sur le port 5310.90.90.90208.80.153.240
Si nous regardons sous quels noms de domaine ces IP se résolvent :
10.90.90.90:settings-win.data.microsoft.com208.80.153.240:upload.wikimedia.org
À ce stade, rien ne semble indiquer un C2. Ces connexions ressemblent davantage à de l’activité système ou utilisateur assez banal.
Nous allons donc tester une autre piste : et si l’accès était venu de l’extérieur vers notre parc ? L’hypothèse 1 n’est donc pas confirmée à ce stade.
Hypothèse 2 : la connexion malveillante vient de l’extérieur vers nos IP internes. Il faut donc chercher les connexions d’IP externes vers nos IP privées :
filtered = conn[
~conn["id.orig_h"].isin(internal_ips) &
conn["id.resp_h"].isin(internal_ips)
]
agg = (
filtered.groupby(["id.orig_h", "id.resp_h", "id.resp_p"])
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
agg[:10]
| id.orig_h | id.resp_h | id.resp_p | count | |
|---|---|---|---|---|
| 4 | 194.61.24.102 | 10.42.85.10 | 3389 | 29309 |
| 0 | 194.61.24.102 | 10.42.85.10 | 0 | 1 |
| 1 | 194.61.24.102 | 10.42.85.10 | 14 | 1 |
| 2 | 194.61.24.102 | 10.42.85.10 | 80 | 1 |
| 3 | 194.61.24.102 | 10.42.85.10 | 443 | 1 |
Là, en revanche, nous avons quelque chose de beaucoup plus intéressant.
L’IP externe 194.61.24.102 se connecte massivement à 10.42.85.10, que nous soupçonnons déjà d’être le contrôleur de domaine, sur le port 3389 (RDP). Voir du RDP arriver depuis Internet sur un contrôleur de domaine n’est déjà pas une très bonne nouvelle. En voir autant, c’est carrément un gyrophare.
Regardons cela de plus près dans les logs RDP :
rdp = datasets["rdp"]
rdp["dt_utc"] = pd.to_datetime(rdp["ts"], unit="s", utc=True)
filtered = rdp[rdp["id.orig_h"] == "194.61.24.102"]
filtered[["dt_utc", "id.orig_h", "id.resp_h", "id.resp_p", "cookie"]][:5]
| dt_utc | id.orig_h | id.resp_h | id.resp_p | cookie | |
|---|---|---|---|---|---|
| 0 | 2020-09-19 02:19:26.549144983+00:00 | 194.61.24.102 | 10.42.85.10 | 3389 | nmap |
| 1 | 2020-09-19 02:21:26.112469912+00:00 | 194.61.24.102 | 10.42.85.10 | 3389 | Administrator |
| 2 | 2020-09-19 02:21:26.342463017+00:00 | 194.61.24.102 | 10.42.85.10 | 3389 | Administrator |
| 3 | 2020-09-19 02:21:26.564666033+00:00 | 194.61.24.102 | 10.42.85.10 | 3389 | Administrator |
| 4 | 2020-09-19 02:21:26.786791086+00:00 | 194.61.24.102 | 10.42.85.10 | 3389 | Administrator |
Nous pouvons remarquer que la première connexion RDP 194.61.24.102 -> 10.42.85.10 a lieu le 2020-09-19 à 02:19:26 UTC.
Le champ cookie indique nmap, ce qui suggère très fortement un repérage initial à l’aide de l’outil éponyme. Deux minutes plus tard, des tentatives de connexion apparaissent avec le compte Administrator, toujours depuis la même IP.
Pris isolément, un nom d’utilisateur dans un champ ne suffit pas à prouver une compromission. En revanche, la séquence scan -> rafale de tentatives RDP -> même compte visé est très difficile à interpréter autrement que comme une tentative de brute force.
Nous pouvons même utiliser une visualisation pour observer ces différentes connexions RDP dans notre dataset :

Nous pouvons aussi faire un histogramme en échelle logarithmique :

À ce stade, notre lecture tient plutôt bien : un scan est visible, puis une rafale de connexions RDP suit immédiatement. L’hypothèse d’un accès initial tenté via RDP est donc fortement confortée.
Compromission du contrôleur de domaine
Si notre lecture est correcte, la question suivante n’est plus vraiment “y a-t-il eu une tentative ?”, mais plutôt “est-ce qu’elle a abouti ?”
Hypothèse 3 : le contrôleur de domaine a bien été compromis, puis utilisé par l’attaquant pour lancer d’autres actions réseau.
Regardons les requêtes émises par 10.42.85.10 à partir de minuit.
moment = pd.Timestamp("2020-09-19 00:00:00", tz="UTC")
filtered = conn[
(conn["id.orig_h"] == "10.42.85.10") &
(conn["dt_utc"] >= moment)
]
agg = (
filtered.groupby(["id.orig_h", "id.resp_h", "id.resp_p"])
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
agg
| id.orig_h | id.resp_h | id.resp_p | count | |
|---|---|---|---|---|
| 3 | 10.42.85.10 | 192.168.45.1 | 53 | 219 |
| 8 | 10.42.85.10 | 224.0.0.252 | 5355 | 51 |
| 2 | 10.42.85.10 | 10.42.85.255 | 138 | 26 |
| 1 | 10.42.85.10 | 10.42.85.255 | 137 | 26 |
| 4 | 10.42.85.10 | 194.61.24.102 | 80 | 4 |
Quelque chose doit nous attirer immédiatement ici : l’Active Directory contacte la machine attaquante sur le port 80.
10.42.85.10 -> 194.61.24.102:80
Et comme le port 80 correspond très probablement à HTTP, nous pouvons utiliser le log Zeek qui parse HTTP pour regarder ce qu’il se passe réellement.
http = datasets["http"]
filtered = http[
(http["id.orig_h"] == "10.42.85.10") &
(http["id.resp_h"] == "194.61.24.102")
]
filtered[["dt_utc", "method", "host", "uri", "uid"]].to_html()
| dt_utc | method | host | uri | uid | |
|---|---|---|---|---|---|
| 119 | 2020-09-19 02:23:41.731918097+00:00 | GET | 194.61.24.102 | / | CJhJ7x4MLfrnnZXrNl |
| 120 | 2020-09-19 02:23:41.797122955+00:00 | GET | 194.61.24.102 | /favicon.ico | CJCXOJ3CW883nvrbL5 |
| 121 | 2020-09-19 02:24:06.939239025+00:00 | GET | 194.61.24.102 | /coreupdater.exe | C9BMVw42pztyZxB25c |
Cette séquence est très parlante : après avoir accédé à http://194.61.24[.]102/, l’hôte télécharge le fichier :
http://194.61.24[.]102/coreupdater.exe
Le téléchargement a lieu à 02:24:06 UTC le 19 septembre 2020. Le navigateur utilisé pourrait être Mozilla Firefox d’après le User-Agent, même si ce champ peut évidemment être falsifié.
Ici, le point important n’est pas tant le navigateur que la logique globale : une machine interne, juste après une phase de tentative d’accès RDP, récupère un exécutable depuis l’infrastructure de l’attaquant. Il devient donc très difficile de soutenir que nous sommes face à une simple activité bénigne.
L’hypothèse 3 est donc fortement confirmée : le contrôleur de domaine semble bien avoir été compromis puis utilisé pour télécharger un outil malveillant.
Hypothèse 4 : s’il s’agit bien d’un attaquant, le logiciel coreupdater.exe est probablement un outil malveillant, voire un loader ou un RAT lui permettant de conserver un accès au réseau.
Utilisons maintenant le dataset Zeek qui liste les fichiers, en pivotant sur l’UID. L’UID est un identifiant unique permettant de retrouver la même session entre tous les fichiers de log Zeek.
files = datasets["files"]
files[files["uid"] == "C9BMVw42pztyZxB25c"][["uid", "extracted", "total_bytes"]].to_html()
| uid | extracted | total_bytes | |
|---|---|---|---|
| 561 | C9BMVw42pztyZxB25c | extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9 | 7168.0 |
Attention les yeux : reverse de très haut vol
Le code précédent nous a indiqué où Zeek a extrait le fichier en question, nommé coreupdater.exe. Nous pouvons maintenant regarder le binaire situé dans extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9.
[nobisd@investmachine network_investigation]$ file extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9
extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9: PE32+ executable for MS Windows 4.00 (GUI), x86-64, 3 sections
Il s’agit bien d’un PE, donc d’un exécutable Windows. Et si nous utilisons le plus grand outil de reverse de tous les temps :
[nobisd@investmachine network_investigation]$ strings extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9
!This program cannot be run in DOS mode.
Rich}E
.text
`.rdata
@.lhru
PAYLOAD:
ExitProcess
VirtualAlloc
KERNEL32.dll
...
KERNEL32.dll
VirtualAlloc
ExitProcess
Nous remarquons que le programme mentionne notamment VirtualAlloc, qui peut tout à fait servir à allouer de la mémoire pour exécuter un second stage en mémoire. À lui seul, ce détail ne suffit pas à classer le binaire. En revanche, remis dans son contexte - téléchargement depuis l’infrastructure attaquante, nom de fichier douteux, puis communications ultérieures - il renforce nettement l’idée que nous avons affaire à un loader.
Nous calculons ensuite son hash et faisons une petite recherche sur VirusTotal :
~/project/blog_content/network_investigation> md5sum extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9
eed41b4500e473f97c50c7385ef5e374 extract_files/extract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX9
En recherchant le hash MD5 sur VirusTotal, nous tombons sur cette analyse. Le fichier est détecté comme malveillant par 64 moteurs sur 72, et l’analyse de sandbox montre que le malware se connecte à l’IP 203.78.103.109 sur le port TCP/443.
Nous venons donc de récupérer un IOC supplémentaire, mais surtout un très bon indicateur pour la suite.
Hypothèse 5 : après avoir été installé sur le poste, le malware coreupdater.exe a communiqué avec son serveur de command and control pour recevoir ses ordres.
Command and Control
Pivotons maintenant sur l’adresse IP trouvée en recherchant les communications qui lui sont destinées :
filtered = conn[conn["id.resp_h"] == "203.78.103.109"]
filtered[["dt_utc", "id.orig_h", "id.resp_h", "id.resp_p", "duration", "orig_bytes", "resp_bytes"]]
| dt_utc | id.orig_h | id.resp_h | id.resp_p | duration | orig_bytes | resp_bytes | |
|---|---|---|---|---|---|---|---|
| 31591 | 2020-09-19 02:25:18.565675974+00:00 | 10.42.85.10 | 203.78.103.109 | 443 | 257.869415 | 31790.0 | 896029.0 |
| 32077 | 2020-09-19 02:29:49.290306091+00:00 | 10.42.85.10 | 203.78.103.109 | 443 | 1377.098415 | 25527.0 | 556338.0 |
| 32722 | 2020-09-19 04:08:45.483779907+00:00 | 10.42.85.115 | 203.78.103.109 | 443 | 5396.529843 | 21655.0 | 461378.0 |
| 32724 | 2020-09-19 02:56:38.402867079+00:00 | 10.42.85.10 | 203.78.103.109 | 443 | 9733.238141 | 54286.0 | 908141.0 |
| 32725 | 2020-09-19 02:40:49.936292887+00:00 | 10.42.85.115 | 203.78.103.109 | 443 | 10674.445423 | 228414.0 | 1674989.0 |
Nous trouvons bien plusieurs communications vers cette IP. De plus, la workstation 10.42.85.115 communique elle aussi avec cette même infrastructure. Cela suggère que la compromission ne s’est pas arrêtée au contrôleur de domaine.
Regardons maintenant si l’IP 203.78.103.109 a communiqué vers notre SI :
filtered = conn[conn["id.orig_h"] == "203.78.103.109"]
filtered[["dt_utc", "id.orig_h", "id.resp_h", "id.resp_p", "duration", "orig_bytes", "resp_bytes"]]
Aucun résultat.
À ce stade, l’hypothèse 5 est fortement confortée : le contrôleur de domaine, puis la workstation, semblent avoir communiqué avec l’infrastructure C2 identifiée via l’échantillon.
Mouvement latéral
Maintenant que nous avons un contrôleur de domaine compromis, la question est jusqu’où l’attaquant est-il allé ?
Hypothèse 6 : après avoir compromis l’Active Directory, l’attaquant a utilisé cet accès pour pivoter vers la workstation 10.42.85.115.
Nous recherchons les connexions RDP entre les deux machines :
rdp[
(rdp["id.orig_h"] == "10.42.85.10") &
(rdp["id.resp_h"] == "10.42.85.115")
][["dt_utc", "id.orig_h", "id.resp_h", "cookie", "security_protocol"]]
| dt_utc | id.orig_h | id.resp_h | cookie | security_protocol | |
|---|---|---|---|---|---|
| 99 | 2020-09-19 02:35:55.291953087+00:00 | 10.42.85.10 | 10.42.85.115 | HYBRID_EX |
Une connexion a bien lieu à 2020-09-19 02:35:55.291953087+00:00.
Attention : une connexion RDP interne, prise seule, n’est pas forcément malveillante. Mais replacée dans la séquence complète d’abord compromission du contrôleur de domaine, le téléchargement du malware, la communications C2, puis l’ouverture d’une session RDP vers la workstation. Cela ne semble pas anodin.
L’hypothèse 6 est donc très probable : nous avons très probablement affaire à un mouvement latéral depuis 10.42.85.10 vers 10.42.85.115.
Action sur objectif
Nous allons maintenant essayer de savoir, ce qu’a bien pu faire l’attaquant une fois sur la workstation.
Hypothèse 7 : après s’être déplacé de 10.42.85.10 vers 10.42.85.115, l’attaquant a utilisé ses nouveaux accès pour interagir avec des ressources sensibles du domaine.
Vérifions cela en analysant les connexions depuis 10.42.85.115 à partir de 02:35.
moment_start = pd.Timestamp("2020-09-19 02:35:00", tz="UTC")
moment_end = pd.Timestamp("2020-09-19 04:00:00", tz="UTC")
filtered = conn[
(conn["id.orig_h"] == "10.42.85.115") &
(conn["dt_utc"] >= moment_start) &
(conn["dt_utc"] <= moment_end)
]
(
filtered.groupby(["id.orig_h", "id.resp_p"])
.size()
.reset_index(name="count")
.sort_values("count", ascending=False)
)
| id.orig_h | id.resp_p | count | |
|---|---|---|---|
| 6 | 10.42.85.115 | 443 | 220 |
| 0 | 10.42.85.115 | 53 | 104 |
| 1 | 10.42.85.115 | 80 | 34 |
| 5 | 10.42.85.115 | 389 | 32 |
| 4 | 10.42.85.115 | 135 | 10 |
| 9 | 10.42.85.115 | 49155 | 10 |
| 2 | 10.42.85.115 | 88 | 9 |
| 7 | 10.42.85.115 | 445 | 9 |
| 3 | 10.42.85.115 | 123 | 5 |
| 8 | 10.42.85.115 | 1900 | 1 |
| 10 | 10.42.85.115 | 49158 | 1 |
Le bruit de fond réseau est toujours là, mais plusieurs ports sont intéressants car ils pourraient être la cible de l’activité malveillante : Kerberos, SMB, LDAP, HTTPS.
Regardons maintenant les connexions Kerberos. Elles peuvent nous donner une idée des services auxquels l’attaquant a cherché à accéder :
moment = pd.Timestamp("2020-09-19 02:35:00", tz="UTC")
filtered = kerberos[
(kerberos["id.orig_h"] == "10.42.85.115") &
(kerberos["dt_utc"] >= moment) &
(kerberos["id.resp_p"] == 88)
]
filtered[:20][["dt_utc", "id.orig_h", "id.resp_h", "request_type", "client", "service", "success"]]
| dt_utc | id.orig_h | id.resp_h | request_type | client | service | success | |
|---|---|---|---|---|---|---|---|
| 42 | 2020-09-19 02:36:24.902311087+00:00 | 10.42.85.115 | 10.42.85.10 | AS | Administrator/C137.LOCAL | krbtgt/C137.LOCAL | 0.0 |
| 43 | 2020-09-19 02:36:24.913220882+00:00 | 10.42.85.115 | 10.42.85.10 | AS | Administrator/C137.LOCAL | krbtgt/C137.LOCAL | 1.0 |
| 44 | 2020-09-19 02:36:24.914736032+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | host/desktop-sdn1rpt.c137.local | 1.0 |
| 45 | 2020-09-19 02:36:24.971004009+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | LDAP/CITADEL-DC01.C137.local/C137.local | 1.0 |
| 46 | 2020-09-19 02:36:24.986465931+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | cifs/CITADEL-DC01 | 1.0 |
| 47 | 2020-09-19 02:36:24.987912893+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | krbtgt/C137.LOCAL | 1.0 |
| 48 | 2020-09-19 02:36:26.761493921+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | cifs/CITADEL-DC01.C137.local | 1.0 |
| 49 | 2020-09-19 02:36:26.767080069+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | ProtectedStorage/CITADEL-DC01.C137.local | 1.0 |
| 54 | 2020-09-19 02:39:10.151730061+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | Administrator/C137.LOCAL | ldap/CITADEL-DC01.C137.local | 1.0 |
| 65 | 2020-09-19 04:08:15.860938072+00:00 | 10.42.85.115 | 10.42.85.10 | AS | ricksanchez/C137 | krbtgt/C137 | 0.0 |
| 66 | 2020-09-19 04:08:15.867786884+00:00 | 10.42.85.115 | 10.42.85.10 | AS | ricksanchez/C137 | krbtgt/C137.LOCAL | 1.0 |
| 67 | 2020-09-19 04:08:15.870420933+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | ricksanchez/C137.LOCAL | host/desktop-sdn1rpt.c137.local | 1.0 |
| 68 | 2020-09-19 04:08:15.881690979+00:00 | 10.42.85.115 | 10.42.85.10 | AS | ricksanchez/C137.LOCAL | krbtgt/C137.LOCAL | 0.0 |
| 69 | 2020-09-19 04:08:15.887491941+00:00 | 10.42.85.115 | 10.42.85.10 | AS | ricksanchez/C137.LOCAL | krbtgt/C137.LOCAL | 1.0 |
| 70 | 2020-09-19 04:08:16.334193945+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | ricksanchez/C137.LOCAL | LDAP/CITADEL-DC01.C137.local/C137.local | 1.0 |
| 71 | 2020-09-19 04:08:16.633737087+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | ricksanchez/C137.LOCAL | cifs/CITADEL-DC01 | 1.0 |
| 72 | 2020-09-19 04:08:16.635783911+00:00 | 10.42.85.115 | 10.42.85.10 | TGS | ricksanchez/C137.LOCAL | krbtgt/C137.LOCAL | 1.0 |
Nous voyons qu’un utilisateur, probablement l’attaquant ou un compte qu’il contrôle, parvient à utiliser Administrator/C137.LOCAL pour obtenir des tickets vers plusieurs services :
host/desktop-sdn1rpt.c137.localLDAP/CITADEL-DC01.C137.local/C137.localcifs/CITADEL-DC01.C137.localProtectedStorage/CITADEL-DC01.C137.local
Ici le service ProtectedStorage peut nous interpeler, pour progresser encore un peu dans notre investigation, regardons les connexions SMB qui suivent :
smb = datasets["smb_mapping"]
moment = pd.Timestamp("2020-09-19 02:35:00", tz="UTC")
filtered = smb[
(smb["id.orig_h"] == "10.42.85.115") &
(smb["dt_utc"] >= moment)
]
filtered[:10][["dt_utc", "path", "share_type", "id.orig_h", "id.resp_h"]]
| dt_utc | path | share_type | id.orig_h | id.resp_h | |
|---|---|---|---|---|---|
| 7 | 2020-09-19 02:36:24.989968061+00:00 | \\\\CITADEL-DC01\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 8 | 2020-09-19 02:36:24.991329908+00:00 | \\\\CITADEL-DC01\ETLOGON | DISK | 10.42.85.115 | 10.42.85.10 |
| 9 | 2020-09-19 02:36:26.765434027+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 10 | 2020-09-19 02:43:21.558957100+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 11 | 2020-09-19 02:58:21.583658934+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 12 | 2020-09-19 03:13:21.587025881+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 13 | 2020-09-19 03:28:21.601809978+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 14 | 2020-09-19 03:30:14.835269928+00:00 | \\\\CITADEL-DC01.C137.local\\sysvol | DISK | 10.42.85.115 | 10.42.85.10 |
| 15 | 2020-09-19 03:43:21.622380972+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 16 | 2020-09-19 03:58:21.654272079+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 17 | 2020-09-19 04:08:16.637520075+00:00 | \\\\CITADEL-DC01\\FileShare | DISK | 10.42.85.115 | 10.42.85.10 |
| 18 | 2020-09-19 04:13:22.089886904+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 19 | 2020-09-19 04:28:22.151868105+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 20 | 2020-09-19 04:43:22.228586912+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 21 | 2020-09-19 04:58:22.265593052+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 22 | 2020-09-19 05:03:14.816766024+00:00 | \\\\CITADEL-DC01.C137.local\\sysvol | DISK | 10.42.85.115 | 10.42.85.10 |
| 23 | 2020-09-19 05:13:22.287499905+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
| 24 | 2020-09-19 05:28:22.357858896+00:00 | \\\\CITADEL-DC01.C137.local\\IPC$ | PIPE | 10.42.85.115 | 10.42.85.10 |
Nous voyons des accès vers IPC$, sysvol puis, plus tard, vers FileShare. C’est cette dernière partie qui nous intéresse le plus, juste après l’authentification Kerberos du compte Administrator.
Regardons également les connexions DCE/RPC :
rpc = datasets["dce_rpc"]
moment = pd.Timestamp("2020-09-19 02:35:00", tz="UTC")
filtered = rpc[
(rpc["dt_utc"] >= moment)
]
filtered[:20]
Je ne remets pas tout le détail ici, mais nous pouvons remarquer à 02:36:26 UTC l’appel d’une procédure nommée bkrp_BackupKey sur l’interface BackupKey. Et là, nous commençons à toucher quelque chose de vraiment intéressant.
D’après la documentation Microsoft, cette procédure permet de récupérer des clés stockées dans l’Active Directory lorsqu’on s’authentifie avec un compte autorisé.
Après lecture de la page de DSInternals dédiée au sujet, il semble que cette API puisse être utilisée pour extraire la clé de sauvegarde DPAPI, c’est-à-dire l’un des mécanismes permettant ensuite de déchiffrer certains secrets stockés localement.
Ici le PCAP ne nous montre pas directement le contenu d’un secret en clair. En revanche, la séquence suivante est très forte :
- compromission probable du contrôleur de domaine ;
- authentification avec
Administrator; - accès Kerberos et SMB cohérents avec une montée en puissance ;
- appel
bkrp_BackupKey; - accès ultérieur au partage
FileShare.
Pris ensemble, ces éléments soutiennent l’idée que l’attaquant a cherché à récupérer - et probablement obtenu - la capacité d’accéder à des secrets protégés par DPAPI.
Nous avons vu précédemment dans les logs SMB qu’à 04:08:16 UTC, l’attaquant se connecte au partage SMB FileShare de l’Active Directory. À ce stade, nous n’avons pas la preuve du contenu exact lu ou exfiltré. En revanche, nous avons une chaîne d’indices suffisamment cohérente pour considérer qu’il a très probablement eu les moyens d’accéder au secret de la sauce Szechuan.
Conclusion
À partir de la seule télémétrie réseau, nous avons pu reconstituer la chaîne d’attaque.
Tout commence par ce qui ressemble à une phase de repérage sur un service RDP exposé. Presque immédiatement après, nous voyons une rafale de tentatives d’authentification visant le compte Administrator. Puis le contrôleur de domaine télécharge coreupdater.exe directement depuis l’infrastructure de l’attaquant. Ce binaire est ensuite relié à une infrastructure de command and control connue, contactée en HTTPS.
Après la compromission probable du contrôleur de domaine, une connexion RDP est observée vers la workstation 10.42.85.115, qui se met ensuite elle aussi à communiquer avec le C2. Nous observons alors des requêtes Kerberos vers des services particulièrement sensibles, des accès SMB cohérents avec une exploration ou une utilisation avancée du domaine, puis un appel bkrp_BackupKey sur l’interface BackupKey, qui suggère une tentative très sérieuse de récupération de secrets liés à DPAPI.
Petite précision : le PCAP ne nous donne pas une preuve absolue de l’exfiltration du fameux secret. En revanche, il nous donne largement de quoi affirmer, avec un niveau de confiance élevé, que l’attaquant a très probablement obtenu les moyens d’accéder à la sauce Szechuan.
Mapping MITRE ATT&CK
Voici le mapping ATT&CK que l’on peut proposer à partir de ce que nous observons dans le PCAP :
T1046 - Network Service Discovery Le scan initial avec Nmap correspond bien à une phase de découverte des services exposés.
T1133 - External Remote Services Sur la phase d’accès initial, le scénario se rapproche d’un abus d’un service distant exposé vers Internet.
T1110 / T1110.001 - Brute Force / Password Guessing La rafale de tentatives RDP sur le compte
Administratorest compatible avec une tentative de brute force par essais successifs de mot de passe.T1105 - Ingress Tool Transfer Le téléchargement de
coreupdater.exedepuis l’infrastructure attaquante vers le contrôleur de domaine correspond bien à un transfert d’outil sur la machine compromise.T1071.001 - Application Layer Protocol: Web Protocols Les communications HTTP/HTTPS avec l’infrastructure attaquante et le serveur de command and control entrent bien dans cette catégorie.
T1021.001 - Remote Services: Remote Desktop Protocol Le pivot observé entre
10.42.85.10et10.42.85.115via RDP correspond très bien à un mouvement latéral par service distant.T1021.002 - Remote Services: SMB/Windows Admin Shares Les accès à
IPC$et aux partages SMB du contrôleur de domaine s’intègrent logiquement dans cette famille de techniques.T1555 - Credentials from Password Stores Pour la partie DPAPI, c’est le mapping le plus raisonnable, mais il faut rester prudent : le PCAP suggère fortement une récupération de capacité d’accès à des secrets protégés, sans nous donner une preuve parfaite du mécanisme exact côté hôte.
T1039 - Data from Network Shared Drive L’accès final à
FileShareest cohérent avec une phase de collecte depuis un partage réseau.