Facendo Scripting a volte nasce la necessità di definire del codice da eseguire su sistemi remoti, per esempio per lanciare comandi, estrarre report o semplicemente per raccogliere informazioni legate alla salute del sistema stesso. Queste attività implicano molto spesso la necessità di aprire delle sessioni su i server target, utilizzando un set di credenziali con i corretti privilegi di esecuzione. Ed è davanti a questa problematica che solitamente gli uffici Sicurezza ICT nonché le strutture di controllo come l’Audit, restano in attesa nel vedere come il sistemista si occuperà della gestione delle password legate agli account. Si, perché ovviamente le credenziali (username e password), dovranno essere passate tramite lo script al sistema remoto per far si che questo possa verificarle. Da qui ritorna il classico dilemma nel capire come definire del buon codice, che possa alzare il livello di sicurezza in contrapposizione al mantenimento delle funzionalità garantite.
A questo punto direi di iniziare a fare alcune riflessioni partendo dai primi concetti per arrivare man mano alla creazione di un modello di gestione che sia sostenibile. Per prima cosa, apriamo una Shell e lanciamo questi comandi:
C:\Users\Jack>$StringaNonSicura = Read-Host "Inserisci una password:"
La variabile chiamata $StringaNonSicura viene definita di tipo System.String e di conseguenza il suo valore viene conservato in chiaro (plaintext). In altri termini, il dato viene gestito come fosse una semplice stringa di testo. E’ ovvio che questa modalità, non può essere adottata nel caso si dovesse definire una variabile password. Oltre a questo c’è da dire che il dato, fintanto che il RunSpace rimarrà attivo (per esempio fino a quando la shell rimane aperta) nella memoria del sistema, potrà essere violato tramite un crash dump o semplicemente copiato sul disco mediante dei tool dedicati. Per ovviare a questo e mantenere quindi il dato in sicurezza, il framework .NET mette a disposizione una classa chiamata SecureString che farà da contenitore per i dati cifrandoli prima di metterli in memoria. Proviamo quindi ad aggiungere un parametro chiamato -AsSecureString al precedente comando, cosi come segue:
PS C:\Users\Jack> $StringaSicura = Read-Host -AsSecureString "Inserisci una password"
Inserisci una password: *****
PS C:\Users\Jack> $StringaSicura
System.Security.SecureString
PS C:\Users\Jack> $StringaNonSicura = Read-Host "Inserisci una password"
Inserisci una password: Pa$$w0rd
PS C:\Users\Jack> $StringaNonSicura
Pa$$w0rd
Aggiungendo questo nuovo parametro, il contenuto della variabile non sarà direttamente leggibile. Invocando infatti la variabile $StringaSicura il relativo output rimarrà nascosto all’interno del contenitore SecureString. Al contrario non specificando il parametro, invocando la variabile $StringaNonSicura il relativo output restituirà il valore in chiaro.
Ora proviamo a fare un passo in più ed introdurre il Cmdlet Get-Credential che viene solitamente utilizzato per acquisire un set di credenziali in maniera interattiva. Proviamo a lanciare quindi il comando:
$Credentials = Get-credential
Questo comando instazia un oggetto di tipo System.Management.Automation.PSCredential di qui le due proprietà sono: Username di tipo String e Password di tipo SecureString.Come si può dedurre, questa seconda proprietà sarà memorizzata in maniera cifrata nella memoria. A questo punto proviamo a fare un primo test, lanciando un semplice comando su un server remoto chiamato Server1:
PS C:\Users\jack> $Credentials = Get-Credential
PS C:\Users\jack> $Result = Invoke-Command -ComputerName Server1 -Credential $Credentials -ScriptBlock {Get-Date}
Alla variabile $Result
sarà restituiro l’output del comando lanciato sul sistema remoto, tramite il Cmdlet Invoke-Command.
L’oggetto System.Management.Automation.PSCredential ha tuttavia una piccola debolezza. Tra i metodi dell’oggetto ne esiste uno chiamato GetNetworkCredentials. Il ritorno di questo metodo è simile all’oggetto NetworkCredential, utilizzato per il passaggio delle credenziali per autenticazione di tipo Basi, NTLM e Kerberos. Una proprietà di questo metodo è chiamato Password il cui valore è appunto il dato password passato dal Cmdlet. Notare l’output del codice sotto riportato:
PS C:\Users\jack> $Credentials.GetNetworkCredential().Password
PS C:\Users\jack> Pa$$w0rd
L’inserimento in modalità interattiva delle credenziali però non può essere un metodo utilizzabile li dove necessario pianificare un automatismo dello script stesso. Vediamo quindi a questo punto come utilizzare dati sensibili come Username e Password in maniera sicura all’interno del codice.
Ripartiamo quindi dal concetto di SecureString. Come detto all’inizio, questo tipo non è altro che un contenitore che permette di gestire in maniera sicura il dato in memoria. Negli esempi precedenti, abbiamo utilizzato il parametro -AsSecureString del cmdlet Read-Host. Cosi facendo il dato viene direttamente convertito dal tipo iniziale System.String. Esiste comunque un Cmdlet dedicato per la conversione del dato direttamente all’interno del codice:
PS C:\Users\jack> $StringaNonSicura = "Pa$$w0rd"
PS C:\Users\jack> $StringaSicura = ConvertTo-SecureString $StringaNonSicura -AsPlainText -Force
$StringaSicura è la conversione della precedente variabile che contiene il testo in chiaro. Il Cmdlet ConvertTo-SecureString è usato per covertire il dato in un oggetto SecureString. In aggiunta, i parametri –AsPlaintText -Force specificano al Cmdlet che il dato passato è in chiaro. Come detto inizialmente, non è possibile salvare questo tipo di oggetto in file da utilizzare successivamente (per come esempio una password cifrata). Per poter quindi procedere nell’riutilizzo, è necessario introdurre un nuovo Cmdlet chiamato ConvertFrom-SecureString. Proviamo quindi ad integrare un ulteriore riga di codice:
PS C:\Users\jack> $StringaCifrata = ConvertFrom-SecureString $StringaSicura
PS C:\Users\jack> Write-Host $StringaCifrata
01000000d08c9ddf0115d1118c7a00c04fc297eb010000007348c1d2ea628d47a0ed72608bf55f150000000002000000000003660000c00000001000
000061455495e2f8ea643b8a905c89a9c1420000000004800000a000000010000000f024a37340f88e1ed2898889966b3fcc20000000cbaf90ea83ad
9b2c6a0c92c31260361bde524713f67f34ff3919beba65560420140000001197b7166444bfe29bfe2d74c32e3655b41f20f7
PS C:\Users\jack>
Come si può vedere dall’output, il dato risulterà cifrato e potrà quindi essere esportato magari su un file di appoggio da riutilizzare successivamente:
PS C:\Users\jack> $StringaCifrata > password.txt
Direi che tutte le informazioni ci sono per creare un primo codice sostenibile, riassemblando le parti viste precedentemente analizzate:
PS C:\Users\jack> $StringaCifrata = Get-Content password.txt
PS C:\Users\jack> $StringaSicura = ConvertTo-SecureString $StringaCifrata
PS C:\Users\jack> $Credentials = New-Object System.Management.Automation.PSCredential("username",$StringaSicura)
PS C:\Users\jack> $Result = Invoke-Command -ComputerName Server1 -Credential $Credentials -ScriptBlock {Get-Date}
Partendo dal file password.txt che contiene la nostra password cifrata, si procederà con la conversione nel tipo SecureString. Diversamente da prima, in questo caso non sarà utilizzato il parametro -AsPlaintext in quanto il dato non è in chiaro ma cifrato. Instanziando poi un nuovo oggetto PSCredential, che abbiamo detto utilizza la SecureString come Pasword, sarà creata la variabile $Credentials passata poi successivamente al Cmdlet Invoke-Command.
Il problema potrebbe sembrare risolto, ma ovviamente la prima contestazione che potrebbe essere fatta (direi anche plausibile) è che il file di Password è posizionato sul sistema dove gira lo scipt. In questo caso si potrebbe ovviare al problema utilizzando un piccolo workround. Si potrebbe per esempio posizionare il file su un altro sistema, magari tramite una share a cui dare i permessi di accesso esclusivamente all’utente che farà girare lo script (per esempio con un Account di Servizio). By design purtroppo questo non è possibile, in quanto il comando non lo permette. Esiste tuttavia un meccanismo aggiuntivo che potrebbe essere risolutore, ovvero l’utilizzo di chiave di cifratura.
Il parametro chiamato -Key viene utilizzato dal Cmdlet ConvertFrom-SecureString per specificare la chiave di cifratura AES. Sono supportate chiavi da 128-bit (16 bytes), 192-bit (24 bytes) or 256-bit key (32 bytes). Per creare una Key sarà necessario definire una nuova variabile cosi come segue:
PS C:\Users\jack> [Byte[]] $key = (1..16)
Proviamo quindi ad utilizzare il Cmdlet per la cifratura della password, utilizzando la chiave definita:
PS C:\Users\jack> $StringaNonSicura = "Pa$$w0rd"
PS C:\Users\jack> $StringaSicura = ConvertTo-SecureString $StringaNonSicura -AsPlainText -Force
PS C:\Users\jack> [Byte[]] $Key = (1..16)
PS C:\Users\jack> $StringaCifrata = ConvertFrom-SecureString $StringaSicura -Key $Key
PS C:\Users\jack> $StringaCifrata
76492d1116743f0423413b16050a5345MgB8AFoAYwBXAEkAZABrAG4AUQBwAHMAYgBtAHAASQBTADAAWQBpAE4AUgBFAHcAPQA9AHwAMABhADAAMgBjADgA
MABmAGEANgA5ADkAYwBhAGQAYwA4AGQAZgBhAGUANABmADkAYgBlADEAMAA5ADUAMQA4ADQAMQBkADUAMQAxADgAYgBiADAAMwBiAGQANwBiADMANAA2ADEA
MAAyAGYAYwAxADAAYQBhAGIAZgAxADUAYQA
A questo punto si potrebbe utilizzare questo codice per definire uno script di supporto, per la creazione di dati cifrati tramite la chiave AES, avendo come Output appunto due file distinti, chiave e dato cifrato:
#Questo Valore indentifica il nome del SET di credenziali e varrà usato come riferimento durante l'utilizzo dei due file
$CredentialName = Read-Host "Nome per identificare il set di credenziali"
#Specifica la lunghezza della chiave (16,24 o 32)
$KeyLenght = Read-Host "Specificare la lunghezza della chiave di cifratura"
#Inserisce il dato da cifrare (es. la Password)
$StringaNonSicura = Read-Host "Immettere il dato"
#Trasforma il dato in chiaro in SecureString
$StringaSicura = ConvertTo-SecureString $StringaNonSicura -AsPlainText -Force
$Key = New-Object Byte[] $KeyLenght
#Genera una chiave Random partendo dalla lunghezza
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
#Cifra la SecureString
$StringaCifrata = ConvertFrom-SecureString $StringaSicura -Key $key
#Esporta la Chiave AES e il dato Cifrato su due file
$StringaCifrata| Out-File $CredentialName+"_DATA"
$key | Out-File $CredentialName+"_KEY"
Salviamo il codice e chiamiamolo EncryptDataFile.ps1 cosi da poterlo richiamare sempre da riga di comando, quando serve generare delle nuove credenziali.
Per concludere l’articolo riprendiamo il primo test eseguito, per recuperare l’ora di sistema da un server remoto. E proviamo a creare uno script funzionale e sicuro utilizzando tutti i meccanismi descritti precedentemente.
Il server source chiamato ScriptServer, contiene lo script da cui verrà aperta una sessione sul server target chiamato Server1. Per accedere a quest’ultimo, utilizzeremo delle credenziali di collegamento: Username: Utente1, Passord: zaq12wsx. Inoltre per aumentare la sicurezza, posizioneremo tali le credenziali su un terzo server chiamato Server2.
Per prima cosa, andremo a creare un file password utilizzando lo script custom salvato prima e chiamato EncryptDataFile.ps1:
PS C:\Users\A617076\Desktop> .\EncryptDataFile.ps1
Nome per identificare il set di credenziali: GetDate
Specificare la lunghezza della chiave di cifratura: 16
Immettere il dato: zaq12wsx
Come Output saranno generati due file, uno che contiene la password cifrata ed uno che contiene la chiave AES (GetDate_DATA e GetDate_KEY). Successivamente andremo poi a posizionare questi due file sul Serer2 utilizzando una share correttamente configurata. Come si accennava all’inizio, i permessi di lettura saranno garantiti esclusivamente all’utente che lancia lo script (per esempio ad un account di servizio nel caso di un task pianificato).
Analizziamo ora il Codice dello script lanciato dal ScriptServer:
#Recupera la password cifrata e chiava AES dal server Server3
$PassCifrata = Get-Content "\\Server2\Share$\GetDate_DATA"
$KeyFile = Get-Content "\\Server2\Share$\GetDate_KEY"
$Username = "domain\jack"
#Converte in SecureString e posiziona in memoria la passwordin utilizando la chiave
$Password = ConvertTo-SecureString $PassCifrata -Key $keyfile
#Crea un oggesso PSCredential passando le credenziali
$Credentials = New-Object System.Management.Automation.PSCredential($Username,$Password)
#Lancia il comando sul server Server 2
$Result = Invoke-Command -ComputerName Server2 -Credential $Credentials -ScriptBlock {Get-Date}
A questo punto dire che lo script è pronto per essere implementato il cui codice mi sembra essere consolidato al fine di un suo futuro riutilizzo. Personalmente l’ho utilizzato parecchie volte, soprattutto li dove dovevo eseguire degli accessi su sistemi Unix/Linux tramite Shell SSH.
Spero che quanto scritto possa essere utile.Viva