Robocopy KO sur partage Azure Files

Azure Files est une solution proposée par Microsoft via Azure permettant de créer des partages SMB liés à des Storage Account. Ainsi, il est possible de pousser ses répertoires partagés sans avoir de serveur de fichiers en tant que tel.

Je travaille actuellement sur un sujet portant sur le déploiement de plusieurs partages portés par Azure Files. Une fois la solution mise en place, ma tâche aura été de faire un robocopy des données locales vers le partage SMB porté par Azure Files. Cependant, en montant le partage sur le serveur de transfert avec un compte ayant les bons privilèges RBAC et NTFS, Robocopy se heurtait à un manque de permissions, y compris en mode backup :

2021/08/10 15:35:59 ERROR 5 (0x00000005) Copying NTFS Security to Destination Directory \\dundermifflin.inc\shares\iso\
Access is denied.

Voici plus ou moins à quoi ressemblait mon instruction Robocopy exécutée par Powershell.

Start-Process "robocopy.exe" -ArgumentList "$src $dst /S /E /COPY:DATSOU /PURGE /MIR /MT:4 /R:1 /W:1" 

Ce qui cause l’erreur de permissions est l’argument S de /copy, ce qui demande à Robocopy de conserver les permissions NTFS des fichiers lors du transfert.

Il faut savoir que les permissions sont habituellement découpées en 2 couches au niveau d’un partage : les privilèges SMB et NTFS. Les permissions sont indépendantes ; si sur un serveur managé il est possible d’intervenir sur les permissions SMB, ce n’est pas le cas avec Azure Files car l’accès est donné au service portant les partages, pas au serveur en lui-même. Par conséquent, les permissions sont redescendues via RBAC. Il existe des rôles Storage File Data SMB qui permettent de placer des droits en lecture et/ou en écriture sur les partages portés par Azure Files et qui positionnent ensuite au niveau SMB les permissions. Dans ce cas, mon compte ayant le rôle Storage File Data SMB Share Elevated Contributor était celui utilisé.

En fouinant un peu sur le net, je suis tombé sur cette question posée sur la section Q&A du site de Microsoft. Un utilisateur a été très récemment confronté au même problème. Cette erreur de permissions est due au fait que Robocopy a besoin de devenir propriétaire du fichier qu’il vient d’écrire à destination pour lui attribuer les permissions NTFS du fichier d’origine. Or, sur Azure Files, même avec un compte ayant les privilèges RBAC et NTFS au maximum, ce n’est pas possible. Robocopy va donc se heurter à un blocage et va renvoyer le message d’erreur plus haut.

En conséquence, pour contourner le problème qui est reconnu — et pour lequel Microsoft travaille sur une solution qui consisterait à donner à un compte un privilège RBAC permettant d’autoriser de devenir propriétaire d’un fichier — Microsoft conseille de monter le partage distant Azure Files avec un compte local d’administration qui lui, possède tous les droits.

Le partage doit donc être monté en tant que AZURE\NomDuStorageAccount ; le mot de passe est l’Access Key qui est générée et déclarée au niveau du Storage Account.

Au niveau du Storage Account, dans la section Security + networking du blade, il suffit de se rendre dans Access keys pour arriver sur cette page. La première clef est le mot de passe du compte local à utiliser. Ainsi, si le compte de stockage s’appelle storagedundermifflin, le partage doit être monté avec l’utilisateur AZURE\storagedundermifflin.

Ce compte possède tous les droits sur le partage et court-circuite les privilèges RBAC. En utilisant ce compte, Robocopy est donc capable de devenir propriétaire des fichiers copiés et d’y appliquer les permissions NTFS d’origine. En exécutant de nouveau le Robocopy, tout s’est bien déroulé et les permissions NTFS ont pu être conservées entre la source et la destination.

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.

Mes premiers pas sur Python — acte III

Afin de pratiquer Python, j’ai trouvé quelques pages d’exercices sur w3resource. Je n’ai pas réalisé tous les exercices présentés mais plutôt une sélection d’entre eux. Dans cet article, je vais présenter mon code pour chacun d’entre eux, la solution officielle n’étant pas forcément la seule valable. Nombre de ces scripts utilisent des arguments pour être fonctionnels ; vous pouvez appeler l’aide intégrée au script en passant -h en argument. J’ai codé ces snippets avec Python 3.9.1 sur macOS.

Continue reading

Mes premiers pas sur Python – acte II

Il y a quelques semaines de cela, je réalisais mes premiers scripts en Python, langage que j’ai toujours voulu apprendre mais pour lequel je manquais de use case concrets, ayant plus l’habitude de Powershell, travaillant sur des environnements Microsoft. Cependant, par curiosité et envie d’apprendre, je planche volontiers à l’occasion sur Python et c’est aujourd’hui pour un besoin purement personnel que j’ai écrit ces deux scripts.

Pour replacer le contexte, je participe à des compétitions de speedrunning de jeux rétro. Un score est calculé en fonction du temps réalisé pour compléter le jeu en question. Récemment, je me suis demandé si il n’était pas intéressant de valoriser les temps d’exécution très rapides en doublant chaque point attribué sous les 2 heures plutôt que d’avoir un traitement linéaire comme actuellement : on part d’une base de 36000 points (1 par seconde, jusqu’à 10 heures) et l’on soustrait un point pour chaque seconde.

Par exemple, pour une partie complétée en 1 heure, 50 minutes et 28 secondes, on a donc un score de 36000 – 6628 (1 x 3600 + 50 x 60 + 28) soit 29372. Avec ce calcul, on a donc 29372 ; mais avec la nouvelle formule, 36000 – 6628 + 572 soit 29944. Les 572 points proviennent des 572 secondes qui séparent le chronomètre des 2 heures.

J’ai donc développé deux scripts Python permettant de faire le calcul. Le premier script demande à l’utilisateur de saisir à la suite les valeurs. Les boucles try et if permettent de faire les contrôles de robustesse nécessaires :

hrCount = input("Hours: ")
mnCount = input("Minutes: ")
seCount = input("Seconds: ")
try:
	hrCount = int(hrCount)
	mnCount = int(mnCount)
	seCount = int(seCount)
except:
	print("Please input integer values as hours, minutes and seconds.")
	quit()
if seCount >= 60:
	print("Seconds value can't be higher than 60.")
	quit()
if mnCount >= 60:
	print("Minutes value can't be higher than 60.")
	quit()
totalSec = hrCount*3600+mnCount*60+seCount
if totalSec > 36000:
	print("Timer can't be higher than 10 hours.")
	quit()
base = 36000 - totalSec
if totalSec < 7200:
	bonus = 7200 - totalSec
else:
	bonus = 0
sc = base + bonus
print(base)
print(bonus)
print(sc)

Le script affiche à la fin le score de base calculé linéairement, le bonus puis le total.

Le deuxième script effectue le même traitement mais avec une approche différente. J’ai voulu alléger un peu le code et les contrôles de robustesse en utilisant le module argparse. Ce module permet d’ajouter des paramètres en ligne de commande au script : ainsi c’est Python lui-même qui va vérifier le type d’entrée de mes arguments (ici, des integer) et permettre d’afficher une aide.

import argparse
parser = argparse.ArgumentParser(description='Counts all seconds under a 2-hour timer twice.')
parser.add_argument('hrCount', metavar='h', type=int, help='Number of hours')
parser.add_argument('mnCount', metavar='m', type=int, help='Number of minutes')
parser.add_argument('seCount', metavar='s', type=int, help='Number of seconds')
args = parser.parse_args()

if args.seCount >= 60:
	print("Seconds value can't be higher than 60.")
	quit()
if args.mnCount >= 60:
	print("Minutes value can't be higher than 60.")
	quit()
totalSec = args.hrCount*3600+args.mnCount*60+args.seCount
if totalSec > 36000:
	print("Timer can't be higher than 10 hours.")
	quit()
base = 36000 - totalSec
if totalSec < 7200:
	bonus = 7200 - totalSec
else:
	bonus = 0
sc = base + bonus
print(base)
print(bonus)
print(sc)

Voici ce que cela donne en exécution. Le paramètre -h est directement intégré et n’a pas à être codé dans le script.

Les deux scripts sont disponibles dans des versions commentées dans l’archive en lien ci-dessous. J’ai utilisé la version 3.9 de Python sous macOS pour les écrire et les utiliser.