Sunday, October 23, 2022

Notes About Powershell: Getting Credentials

Sometimes when writing a Powershell script you need user credentials. Here are a few ways of doing that. We'll be using "johndoe" as the username, and "SuperSecret" as the password.

The Simplest Way - Hardcode them in your command

This is also the ugliest and least secure way. In this hypothetical case, the commands simply expect the username and password as arguments to the command:

PS > command_1 "johndoe" "SuperSecret"
PS > command_2 "johndoe" "SuperSecret"

Use Variables

Not quite as ugly, but still ugly.

PS > $UserName = "johndoe"
PS > $Password = "SuperSecret"
PS > command_1 $UserName $Password
PS > command_2 $UserName $Password

Prompt the User for a PSCredential Object

Now we're getting to a more secure option.

PS > $Creds = Get-Credential

This will pop up a window, prompting the user to enter his username and password. The script can then access the username with:

$Creds.UserName

and the password with:

$Creds.Password

Assuming our commands take credentials in the format of a PSCredential object, the commands might look like this:

PS > command_1 $Creds
PS > command_2 $Creds

But if they require an actual username/password, you might think that this will work:

PS > command_1 $Creds.UserName $Creds.Password
PS > command_2 $Creds.UserName $Creds.Password

But it won't, because the "$Creds.Password" value is in a special format itself, called a "SecureString". You can see this by simply typing the name of the variable:

PS > $Creds = Get-Credential
cmdlet Get-Credential at command pipeline position 1
Supply values for the following parameters:

PS > $Creds

UserName                     Password
--------                     --------
johndoe  System.Security.SecureString


PS > $Creds.UserName
johndoe

PS > $Creds.Password
System.Security.SecureString

But it's easy to convert that SecureString back into plain text:

PS > $Creds.GetNetworkCredential().Password
SuperSecret

So now our commands would become:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PS > command_2 $Creds.UserName $Creds.GetNetworkCredential().Password
 
Ideally our command would be a Powershell command that natively understands the PSCredential format:

PS > PScommand_1 --Credential $Creds 

Load Up a PSCredential Object From Within the Script

This is another insecure method, but should be known about.

Since, as mentioned before, the Credential object requires a SecureString for the password, we first need to create a SecureString password:

$pwd = ConvertTo-SecureString "SuperSecret" -AsPlainText -Force

Normally the "ConvertTo-SecureString" routine expects its input to be in the format of an encrypted string of text. It's "native languages" are SecureString and Encrypted String. You can see the Encrypted String format by converting the other way:

$pwd | ConvertFrom-SecureString

which will produce something that looks like this:

01000000d08c9ddf0115d1118c7a00c04fc297eb01000000fde23916e5b4734ab18e12ca886af24900000000020000000000106600000001000020000000ff6caf244bddbe34bc6f7c8b9768317738d85d9008de0d3acaac4ee9133bc4870
00000000e80000000020000200000008ce0cd22aa3fcbb938f08f640201e1bb1c91500b679346a02e48815a385101a2200000006e2055f816b37e010e1938e59938d244c6170307ef651db8e95920f0c06b16d540000000f6b72c9148c628
c579209cd761fd0a2d6ebbdfbae888462733b06f08aec293e02f3a62b83ded1a10d24e370ab5b584bc31bf1816273e8761577b0a7455545881

So to be clear, the "ConvertTo.../...From..." routines convert (natively) between SecureString and Encrypted Strings, not to/from plain text. To include the plain text in the process, we have to tell the "ConvertTo..." routine that its input is "AsPlainText", and to get the plain text back out, we have to use a completely different routine, as we did above, the "GetNetworkCredential()" routine.

Now that we have the password as a SecureString, we can create the PSCredential object:

$Creds = New-Object System.Management.Automation.PSCredential ("johndoe", $pwd)

And now you can use the PSCredential object just as we did above:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PC > PScommand_1 --Credential $Creds

Load Up a PSCredential Object From an External File

This is kind of a compromise between the security of prompting the user for the credentials and having the credentials associated with the script itself. The password will be in an Encrypted format, and in a separate file, but it would still be pretty easy for a "hacker" to grab the file and read the password. So it's certainly not secure. You could put the external file on a secure server, but then you'd have to deal with the credentials for logging into the secure server, so all that does is kick the problem down the road a ways. Still, it might be the best we can do without going to extremes.

But the most onerous caveat about this method, at least for general purpose scripts, is that the password encryption can only be decrypted by the same user account on the same computer as was used to do the encryption. You can't write/develop the script that uses this method on one computer, and then run it on another computer, or as a different user.

Some of the following is likely a repeat of what was said above; it's a copy-and-paste from another article I had started/

Reading the Creds From a File

If you don't have a user sitting in front of the computer when the script runs, you can store the username and an encrypted form of the password in a file, beforehand, and then read it from that file when it's needed. Be aware that this is still not secure, but it's more so.

Saving the Credentials to an External File, Beforehand

Not in your script, but just at a Powershell prompt, enter the following:

PS > Set-Content -Path ".\extras.zip" -Value "johndoe"

This step creates a file named "extras.zip", over-writing any existing files of that name, in the current directory. The file contains the username. You can verify the contents of this newly-created file with:

PS > type ".\extras.zip"

As you can see, it's not really a .zip file; it's just a plain text file. But naming it as a .zip is a minor mindgame, hoping to discourage the casual "hackers" from bothering to try and open / look into the file. It's a weak form of "security via obscurity"; probably completely useless, but I know when I'm casually looking at files with which I'm unfamiliar, I tend to look at .txt files and to leave .zip files along. But you can name your file any way you want.

PS > Add-Content -Path ".\extras.zip" -Value $("SuperSecret") | ConvertFrom-SecureString -AsPlainText -Force)

This step adds the username to the now-already-existing "extras.zip" file. The value that gets added to the file is the password, which before getting added to the file, is piped to a converter that takes the plain text of "SuperSecret" and converts it to a SecureString.

You should now have a file named "extras.zip" with something like the following (which you can see with "type .\extras.zip":

johndoe 01000000d08c9ddf0115d1118c7a00c04fc297eb010000001150a84662a6cd45af512286977fabcc00000000020000000000106600000001000020000000943962d675717d4a12cfe44a8a22b6a5f0ca77762c7bb61e6929515af684db5d000000000e80000000020000200000006370779b6f82983d4f417eccad5be5a80b0ae8a71d2820e736862d2b25453ce220000000c96767ea12fd28ab840fcefb49b4f86d84e97f4676e806a8d7511a86532d3c3f400000008a26d44b1d1df760436be9be186cb87a6cca6220364752e435af61188330043ac201f634ff88fd751b17aff9394c00e6ada09a2f1dd93c27702f9802e813f90d

Normally the ConvertTo-SecureString expects its input to be a standard encrypted string of plain text, like the line with numbers above. But we're starting with the plain text of "SuperSecret". If you feed it this plain unencrypted text, it complains. You can see this for yourself with this command:

PS > "SuperSecret" | ConvertTo-SecureString

That's why we use the "-AsPlainText" argument, to override that behavior. You can see this for yourself like so:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText

But then you get another complaint. For security reasons, the command doesn't like to be given secret info out in the open, but you can force it to accept the input anyway, with:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force

and now you see that the result is a SecureString.

However, we can not write this SecureString out to a file, and then recover it later. But what we can do is reverse the process, part-way. We won't reverse it back to a plain-text "SuperSecret", but rather to an encrypted-text form of "SuperSecret", by piping the above command to the counterpart:

PS > 'SuperSecret" | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString

The "native language" of these two tools is SecureStrings on one side and encrypted text on the other, so we don't have to specify anything special; it just takes the SecureString in the previous command and converts from that into encrypted text.

Now that we have the credentials stored in an external file, we have to tell our script to read them.

Reading the Credentials from an External File
PS > $contents = Get-Content -Path ".\extras.zip"
PS > $Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $contents[0], $($contents[1] | ConvertTo-SecureString)

Use the PSCredential Object

And now you can use the PSCredential object just as we did above:

PS > command_1 $Creds.UserName $Creds.GetNetworkCredential().Password
PC > PScommand_1 --Credential $Creds

No comments: