Image de couverture

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.102 semble effectuer un scan Nmap sur notre serveur Active Directory (10.42.85.10)
  • 02:21:26 UTC : l’IP publique 194.61.24.102 ré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 :

Déroulé de l’attaque

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_hid.resp_hid.resp_pprotoservicetotal_bytesorig_bytesresp_bytesduration
3170910.42.85.115104.119.185.124443tcpssl37991129.01858.037989271.0109.659907
32078194.61.24.10210.42.85.103389tcpssl12853439.02366216.010487223.01810.715048
3208910.42.85.1010.42.85.1153389udprdpeudp6448171.073592.06374579.0950.971623
169810.42.85.115104.18.12.165443tcpssl4664235.02328.04661907.0126.184417
134010.42.85.11523.47.193.50443tcpssl4596703.04575.04592128.033.567848

Ces statistiques suggèrent déjà la présence de deux IP internes principales :

  • 10.42.85.115
  • 10.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_hid.resp_hid.resp_pcount
110.42.85.11510.42.85.1053656
510.42.85.11510.42.85.10389128
210.42.85.11510.42.85.108842
710.42.85.11510.42.85.104915540
410.42.85.11510.42.85.1013540

D’après les données précédentes, il semble que :

  • 10.42.85.115 soit une workstation
  • 10.42.85.10 soit 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_hid.resp_hid.resp_pcount
210.42.85.10192.168.45.153584
1810.42.85.11510.90.90.90443354
15210.42.85.115208.80.153.24044372
810.42.85.10224.0.0.252535567
14610.42.85.115204.79.197.20344347
25810.42.85.11572.21.91.298046
15110.42.85.115208.80.153.22444345
21310.42.85.11540.91.125.044340
14410.42.85.115204.79.197.20044339
25410.42.85.11570.37.74.644338

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 53
  • 10.90.90.90
  • 208.80.153.240

Si nous regardons sous quels noms de domaine ces IP se résolvent :

  • 10.90.90.90 : settings-win.data.microsoft.com
  • 208.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_hid.resp_hid.resp_pcount
4194.61.24.10210.42.85.10338929309
0194.61.24.10210.42.85.1001
1194.61.24.10210.42.85.10141
2194.61.24.10210.42.85.10801
3194.61.24.10210.42.85.104431

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_utcid.orig_hid.resp_hid.resp_pcookie
02020-09-19 02:19:26.549144983+00:00194.61.24.10210.42.85.103389nmap
12020-09-19 02:21:26.112469912+00:00194.61.24.10210.42.85.103389Administrator
22020-09-19 02:21:26.342463017+00:00194.61.24.10210.42.85.103389Administrator
32020-09-19 02:21:26.564666033+00:00194.61.24.10210.42.85.103389Administrator
42020-09-19 02:21:26.786791086+00:00194.61.24.10210.42.85.103389Administrator

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 :

Plot

Nous pouvons aussi faire un histogramme en échelle logarithmique :

Histo

À 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_hid.resp_hid.resp_pcount
310.42.85.10192.168.45.153219
810.42.85.10224.0.0.252535551
210.42.85.1010.42.85.25513826
110.42.85.1010.42.85.25513726
410.42.85.10194.61.24.102804

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_utcmethodhosturiuid
1192020-09-19 02:23:41.731918097+00:00GET194.61.24.102/CJhJ7x4MLfrnnZXrNl
1202020-09-19 02:23:41.797122955+00:00GET194.61.24.102/favicon.icoCJCXOJ3CW883nvrbL5
1212020-09-19 02:24:06.939239025+00:00GET194.61.24.102/coreupdater.exeC9BMVw42pztyZxB25c

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()
uidextractedtotal_bytes
561C9BMVw42pztyZxB25cextract-1600482246.939862-HTTP-F15zmh1fD5AVKS9HX97168.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_utcid.orig_hid.resp_hid.resp_pdurationorig_bytesresp_bytes
315912020-09-19 02:25:18.565675974+00:0010.42.85.10203.78.103.109443257.86941531790.0896029.0
320772020-09-19 02:29:49.290306091+00:0010.42.85.10203.78.103.1094431377.09841525527.0556338.0
327222020-09-19 04:08:45.483779907+00:0010.42.85.115203.78.103.1094435396.52984321655.0461378.0
327242020-09-19 02:56:38.402867079+00:0010.42.85.10203.78.103.1094439733.23814154286.0908141.0
327252020-09-19 02:40:49.936292887+00:0010.42.85.115203.78.103.10944310674.445423228414.01674989.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_utcid.orig_hid.resp_hcookiesecurity_protocol
992020-09-19 02:35:55.291953087+00:0010.42.85.1010.42.85.115HYBRID_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_hid.resp_pcount
610.42.85.115443220
010.42.85.11553104
110.42.85.1158034
510.42.85.11538932
410.42.85.11513510
910.42.85.1154915510
210.42.85.115889
710.42.85.1154459
310.42.85.1151235
810.42.85.11519001
1010.42.85.115491581

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_utcid.orig_hid.resp_hrequest_typeclientservicesuccess
422020-09-19 02:36:24.902311087+00:0010.42.85.11510.42.85.10ASAdministrator/C137.LOCALkrbtgt/C137.LOCAL0.0
432020-09-19 02:36:24.913220882+00:0010.42.85.11510.42.85.10ASAdministrator/C137.LOCALkrbtgt/C137.LOCAL1.0
442020-09-19 02:36:24.914736032+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALhost/desktop-sdn1rpt.c137.local1.0
452020-09-19 02:36:24.971004009+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALLDAP/CITADEL-DC01.C137.local/C137.local1.0
462020-09-19 02:36:24.986465931+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALcifs/CITADEL-DC011.0
472020-09-19 02:36:24.987912893+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALkrbtgt/C137.LOCAL1.0
482020-09-19 02:36:26.761493921+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALcifs/CITADEL-DC01.C137.local1.0
492020-09-19 02:36:26.767080069+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALProtectedStorage/CITADEL-DC01.C137.local1.0
542020-09-19 02:39:10.151730061+00:0010.42.85.11510.42.85.10TGSAdministrator/C137.LOCALldap/CITADEL-DC01.C137.local1.0
652020-09-19 04:08:15.860938072+00:0010.42.85.11510.42.85.10ASricksanchez/C137krbtgt/C1370.0
662020-09-19 04:08:15.867786884+00:0010.42.85.11510.42.85.10ASricksanchez/C137krbtgt/C137.LOCAL1.0
672020-09-19 04:08:15.870420933+00:0010.42.85.11510.42.85.10TGSricksanchez/C137.LOCALhost/desktop-sdn1rpt.c137.local1.0
682020-09-19 04:08:15.881690979+00:0010.42.85.11510.42.85.10ASricksanchez/C137.LOCALkrbtgt/C137.LOCAL0.0
692020-09-19 04:08:15.887491941+00:0010.42.85.11510.42.85.10ASricksanchez/C137.LOCALkrbtgt/C137.LOCAL1.0
702020-09-19 04:08:16.334193945+00:0010.42.85.11510.42.85.10TGSricksanchez/C137.LOCALLDAP/CITADEL-DC01.C137.local/C137.local1.0
712020-09-19 04:08:16.633737087+00:0010.42.85.11510.42.85.10TGSricksanchez/C137.LOCALcifs/CITADEL-DC011.0
722020-09-19 04:08:16.635783911+00:0010.42.85.11510.42.85.10TGSricksanchez/C137.LOCALkrbtgt/C137.LOCAL1.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.local
  • LDAP/CITADEL-DC01.C137.local/C137.local
  • cifs/CITADEL-DC01.C137.local
  • ProtectedStorage/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_utcpathshare_typeid.orig_hid.resp_h
72020-09-19 02:36:24.989968061+00:00\\\\CITADEL-DC01\\IPC$PIPE10.42.85.11510.42.85.10
82020-09-19 02:36:24.991329908+00:00\\\\CITADEL-DC01\ETLOGONDISK10.42.85.11510.42.85.10
92020-09-19 02:36:26.765434027+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
102020-09-19 02:43:21.558957100+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
112020-09-19 02:58:21.583658934+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
122020-09-19 03:13:21.587025881+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
132020-09-19 03:28:21.601809978+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
142020-09-19 03:30:14.835269928+00:00\\\\CITADEL-DC01.C137.local\\sysvolDISK10.42.85.11510.42.85.10
152020-09-19 03:43:21.622380972+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
162020-09-19 03:58:21.654272079+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
172020-09-19 04:08:16.637520075+00:00\\\\CITADEL-DC01\\FileShareDISK10.42.85.11510.42.85.10
182020-09-19 04:13:22.089886904+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
192020-09-19 04:28:22.151868105+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
202020-09-19 04:43:22.228586912+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
212020-09-19 04:58:22.265593052+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
222020-09-19 05:03:14.816766024+00:00\\\\CITADEL-DC01.C137.local\\sysvolDISK10.42.85.11510.42.85.10
232020-09-19 05:13:22.287499905+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.42.85.10
242020-09-19 05:28:22.357858896+00:00\\\\CITADEL-DC01.C137.local\\IPC$PIPE10.42.85.11510.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 :

  1. compromission probable du contrôleur de domaine ;
  2. authentification avec Administrator ;
  3. accès Kerberos et SMB cohérents avec une montée en puissance ;
  4. appel bkrp_BackupKey ;
  5. 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 Administrator est compatible avec une tentative de brute force par essais successifs de mot de passe.

  • T1105 - Ingress Tool Transfer Le téléchargement de coreupdater.exe depuis 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.10 et 10.42.85.115 via 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 à FileShare est cohérent avec une phase de collecte depuis un partage réseau.


Références