Powershell : expiration des secrets et certificats des applications Azure ActiveDirectory

Ce script permet d’afficher la liste des App Registrations Azure ActiveDirectory ayant un ou des secrets ou certificats expirant à 90 jours, 30 jours ou déjà expirés.

Il est bien évidemment possible d’adapter le script pour le faire tourner dans un runbook Azure ou sur un serveur de manière non-interactive en adaptant la partie authentification.

Connect-AzureAD
$AADAppsColl = Get-AzureADApplication -All:$true
foreach($AADApps in $AADAppsColl) {
    $AppID = $AADApps.AppID
    $AADApp = Get-AzureADApplication -Filter "AppID eq '$AppID'"
    $PassCreds = $AADApp.PasswordCredentials
    if ($null -ne $PassCreds) {
        foreach($PassCred in $PassCreds) {
            if($PassCred.EndDate -gt (Get-Date).AddDays(30) -and $PassCred.EndDate -le (Get-Date).AddDays(90)){
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "KeyID: $($PassCred.KeyID)"
                Write-Host "Expires: $($PassCred.EndDate)" -ForegroundColor Green
                Write-Host `r
            }
            if($PassCred.EndDate -gt (Get-Date) -and $PassCred.EndDate -le (Get-Date).AddDays(30)) {
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "KeyID: $($PassCred.KeyID)"
                Write-Host "Expires: $($PassCred.EndDate)" -ForegroundColor Orange
                Write-Host `r
            }
            if($PassCred.EndDate -le (Get-Date)) {
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "KeyID: $($PassCred.KeyID)"
                Write-Host "Expired: $($PassCred.EndDate)" -ForegroundColor Red
                Write-Host `r
            }
        }
    }
    $KeyCreds = $AADApp.KeyCredentials
    if ($null -ne $KeyCreds) {
        foreach($KeyCred in $KeyCreds) {
            if($KeyCred.EndDate -gt (Get-Date).AddDays(30) -and $KeyCred.EndDate -le (Get-Date).AddDays(90)){
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "Certificate ID: $($KeyCred.KeyID)"
                Write-Host "Expires: $($KeyCred.EndDate)" -ForegroundColor Green
                Write-Host `r
            }
            if($KeyCred.EndDate -gt (Get-Date) -and $KeyCred.EndDate -le (Get-Date).AddDays(30)) {
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "Certificate ID: $($KeyCred.KeyID)"
                Write-Host "Expires: $($KeyCred.EndDate)" -ForegroundColor Orange
                Write-Host `r
            }
            if($KeyCred.EndDate -le (Get-Date)) {
                Write-Host "AzureAD Application Name: $($AADApp.DisplayName)"
                Write-Host "Certificate ID: $($KeyCred.KeyID)"
                Write-Host "Expired: $($KeyCred.EndDate)" -ForegroundColor Red
                Write-Host `r
            }
        }
    }
}

Une version commentée du script est disponible en téléchargement.

Powershell : Robocopy Log Parser

Cela faisait quelques temps que j’avais cette idée dans mon backlog cérébral, mais je m’y suis vraiment penché aujourd’hui car il a fallu que je contrôle un gros nombre de fichiers de log générés par une instruction Robocopy (350+).

Le principal challenge pour ce script est d’arriver à gérer correctement les lignes de statistiques de Robocopy puisque l’export est au simple format texte :

------------------------------------------------------------------------------

               Total    Copied   Skipped  Mismatch    FAILED    Extras
    Dirs :      3765      3765         0         0         0         0
   Files :    243503    243503         0         0         0         0
   Bytes :  19.238 g  19.238 g         0         0         0         0
   Times :  44:22:58   6:23:45                       0:00:00   0:26:35
   Ended : Monday, February 30, 1998 3:25:57 AM

Ces lignes ne sont pas directement interprétables comme telles par Powershell, il faut donc jouer du regex pour arriver à obtenir les valeurs qui nous intéressent. Si au départ je voulais simplement ressortir les FAILED, l’effort supplémentaire pour sortir l’intégralité des valeurs pour les répertoires et les fichiers est minime, ce script va donc inclure dans le fichier CSV de sortie l’intégralité des statistiques pour les répertoires et fichiers.

function parse {
    param($inputstr)
    $newstr = $inputstr -replace "[^0-9]" , '-'
    $newstr -match '[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)[^0-9]+([0-9]+)'
    $stats = @($matches[1],$matches[2],$matches[3],$matches[4],$matches[5],$matches[6])
    return $stats
}

$output=@()
$logfiles = Get-ChildItem "*.log" | Select-Object Name
foreach($logfile in $logfiles) {
    Write-Host "Parsing $($logfile.Name)"
    $logcontent = Get-Content $logfile.Name
    $strf = $logcontent[$logcontent.Count-5]
    $strd = $logcontent[$logcontent.Count-6]
    $statf = parse($strf)
    $statd = parse($strd)
    $dump = New-Object PSCustomObject
    $dump | Add-Member -Name "Filename" -Value $($logfile.Name) -MemberType NoteProperty
    $dump | Add-Member -Name "Files Total" -Value $statf[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Copied" -Value $statf[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Skipped" -Value $statf[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Mismatched" -Value $statf[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Files FAILED" -Value $statf[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Files Extras" -Value $statf[6] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Total" -Value $statd[1] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Copied" -Value $statd[2] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Skipped" -Value $statd[3] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Mismatched" -Value $statd[4] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs FAILED" -Value $statd[5] -MemberType NoteProperty
    $dump | Add-Member -Name "Dirs Extras" -Value $statd[6] -MemberType NoteProperty
    $output+=$dump
}
$output | Export-Csv robocopy_logs_stats.csv -Delimiter ";" -Encoding utf8

Le script récupère la liste des fichiers en .log dans le répertoire d’exécution pour ensuite les lire. Par chance, les logs Robocopy ont toujours la même structure, ce qui permet d’avoir des valeurs fixes pour séparer les colonnes et récupérer les valeurs qui nous intéressent.

Ensuite, le fichier CSV est facilement interprété par Excel et on peut voir en un clin d’oeil les éventuelles erreurs de transfert et regarder dans le log concerné quels fichiers ou répertoires sont en échec.

Une version commentée du script est disponible en téléchargement.

PrintNightmare – CVE-2021-34528

Microsoft a publié une vulnérabilité dans le service spouleur d’impression, dont tous les détails peuvent se retrouver à ce lien. Il est important de prendre cette faille au sérieux car elle permet notamment d’exécuter du code en tant qu’utilisateur SYSTEM ce qui peut être désastreux en fonction de la machine sur laquelle l’exécution est réalisée.

Plusieurs workarounds sont proposés par Microsoft, et ayant mis en place les corrections nécessaires, je vais partager les quelques scripts (Powershell 5.1) que j’ai conçu afin de me faciliter la tâche.

Tout d’abord, à partir d’un fichier CSV d’inventaire, ce script va interroger le système distant pour récupérer l’état complet du service Spooler (état actuel, type de démarrage, etc.). Ainsi, il est possible d’avoir un état des lieux sur l’exécution de ce service.

$inv = Import-Csv inventaire.csv
$output = @()
foreach($srv in $inv){
    Write-Output $srv.hostname
    $output+=Get-Service -Name Spooler -Computer $srv.hostname
}
$output | Export-Csv spooler_status.csv -Delimiter ";" -Encoding UTF8

Ce deuxième script va appliquer le workaround conseillé par Microsoft ; il s’agit de l’extinction du service Spooler et la désactivation du démarrage automatique de celui-ci. Le script interroge ensuite de nouveau le service pour obtenir son état et génère un fichier CSV de retour.

$inv = Import-Csv spooler_off.csv -Delimiter ";"
$output = @()
foreach($srv in $inv){
    Write-Host $srv.MachineName
    Stop-Service -InputObject $(Get-Service -Name Spooler -ComputerName $srv.MachineName)
    Set-Service -Name Spooler -StartupType Disabled -ComputerName $srv.MachineName
    $srvstat = New-Object PSCustomObject
    $srvstat | Add-Member -Name "Hostname" -Value $srv.MachineName -MemberType NoteProperty
    $srvstat | Add-Member -Name "SpoolerStatus" -Value (Get-Service -Name Spooler -ComputerName $srv.MachineName).Status -MemberType NoteProperty
    $output+=$srvstat
}
$output | Export-Csv spooler_processed.csv -Encoding UTF8 -Delimiter ";"

Et enfin, puisque Microsoft a mis à jour aujourd’hui la vulnérabilité en incluant un contrôle de clefs de registre qui n’était pas présent hier, ce troisième script va interroger les serveurs d’une liste au format CSV afin de récupérer l’état des clefs de registre mettant à risque le système.

$output = @()
$inv = Import-Csv inventaire_hostname.csv -Delimiter ";"
foreach($srv in $inv){
    Write-Host $srv.Hostname
    $pnp = New-Object PSCustomObject
    if(Invoke-Command -ComputerName $srv.Hostname { Test-Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" }){
        $pnp | Add-Member -Name "Hostname" -Value $srv.Hostname -MemberType NoteProperty
        $pnp | Add-Member -Name "PointAndPrintKeyExists" -Value "Yes" -MemberType NoteProperty  
        if((Invoke-Command -ComputerName $srv.Hostname { Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" -Name NoWarningNoElevationOnInstall }) -eq 1) {
            $pnp | Add-Member -Name "NoWarningNoElevationOnInstall" -Value "1" -MemberType NoteProperty
        }
        else { $pnp | Add-Member -Name "NoWarningNoElevationOnInstall" -Value "0" -MemberType NoteProperty }
        if((Invoke-Command -ComputerName $srv.Hostname {Get-ItemPropertyValue "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Printers\PointAndPrint\" -Name NoWarningNoElevationOnUpdate }) -eq 1) {
            $pnp | Add-Member -Name "NoWarningNoElevationOnUpdate" -Value "1" -MemberType NoteProperty
        }
        else { $pnp | Add-Member -Name "NoWarningNoElevationOnUpdate" -Value "0" -MemberType NoteProperty }
    }
    else {$pnp | Add-Member -Name "Hostname" -Value $srv.Hostname -MemberType NoteProperty ; $pnp | Add-Member -Name "PointAndPrintKeyExists" -Value "No or Server Unreacheable" -MemberType NoteProperty }
    $output+=$pnp
}
$output | Export-Csv pointnprint.csv -Delimiter ";" -Encoding UTF8 

En fonction du nombre de serveurs à interroger, l’exécution du script peut être relativement longue. Par souci de rapidité de développement, je n’ai pas inclus de contrôle de réponse du serveur avant d’utiliser Invoke-Command. Il est donc possible que Powershell retourne une erreur si le serveur ne répond pas à la requête ou si il la rejette, cela n’impacte pas le bon déroulement du script.

Naturellement, une étude d’impact et de faisabilité est à faire avant toute intervention et extinction d’un service, même pour le spouleur qui n’est pas d’une importance capitale sur une grande majorité de machines. Il ne reste donc plus qu’à attendre l’application des correctifs de sécurité publiés par Microsoft.

Manipulation du pagefile avec Powershell et scripting diskpart

Récemment, j’ai modifié mon template de machine virtuelle Windows Server porté par VMware pour y intégrer un deuxième disque virtuel afin d’y placer le fichier d’échange de Windows. Seulement, après génération de mon image Windows avec sysprep, mon deuxième disque n’est plus monté sur le système, et par conséquent ma configuration de fichier d’échange est invalide. J’ai donc dû traiter ce problème en deux temps : la première étape a été de faire en sorte de faire un batch pour diskpart afin de monter mon deuxième disque et l’initialiser correctement, et la deuxième étape a été de configurer le pagefile via Powshell afin de pouvoir intégrer le tout dans mon script de post-installation.

Tout d’abord, j’ai appris qu’il était possible de faire avaler à diskpart un fichier texte contenant les instructions qu’il doit exécuter à la suite. Par exemple, si je veux initialiser mon deuxième disque et affecter la lettre E à ma partition, je vais créer un fichier texte nommé « diskinit.txt » contenant ceci :

select disk 1
online disk
select partition 1
assign letter=E

Je conseille tout de même de tester une par une les commandes à la main pour s’assurer que cela fonctionne avant de les placer à la suite dans le fichier. Une fois que le fichier existe, il est possible d’appeler diskpart pour lui demander de traiter celui-ci, grâce au paramètre /s :

diskpart.exe /s diskinit.txt

Puisque cela fonctionne, j’ai dû m’atteler à la gestion du fichier d’échange sous Powershell. Il n’y a pas de commandlet natif sous Windows Server 2016 pour gérer le fichier d’échange comme un paramètre système standard. Il faut passer par WMI. Cette commande permet d’afficher les fichiers d’échange configurés sur la machine, uniquement si le fichier d’échange n’est pas gérée de manière automatique pour tous les disques du système :

Get-CimInstance -ClassName Win32_PagefileSetting

Maintenant que j’ai bien mon disque E:, je peux rajouter mon fichier d’échange sur cette partition et retirer celui présent sur le disque système.

New-CimInstance -ClassName Win32_PagefileSetting -Property @{Name "E:\pagefile.sys"}
Get-CimInstance -ClassName Win32_PagefileSetting | Where-Object { $_.Name -like "C:\pagefile.sys" } | Remove-CimInstance

A noter que cette instruction créée un fichier d’échange dont la taille est gérée par le système. Il est possible de spécifier une taille minimale et/ou maximale, en procédant par exemple comme ceci pour une taille mini de 1024 Mo et une taille maxi de 2048 Mo :

Get-CimInstance -ClassName Win32_PagefileSetting | Where-Object { $_.Name -like "E:\pagefile.sys" } | Set-CimInstance -Property @{InitialSize = 1024; MaximumSize = 2048}

Alternativement, ce qui est passé dans le dernier pipe peut être passé en pipe de l’instruction New-CimInstance exécutée en premier, afin d’avoir le bon réglage dès la création.

Vous pouvez trouver avoir bien plus de détails sur cette classe WMI et un script complet de gestion du pagefile via Powershell en suivant ce lien qui m’a aidé à comprendre et à scripter cette opération. 🙂