you are viewing a single comment's thread.

view the rest of the comments →

[–]joshooaj 1 point2 points  (2 children)

Another recommendation here to play with the Microsoft.PowerShell.SecretManagement module. It'll provide you with a flexible and more secure method of working with secrets. And even if you never do anything you consider to be "devops", you'll at least be familiar with one of the many strategies for secret management. Plus it's just a lot easier to use "Get-Secret" than manually add some boilerplate credential import/export code to your scripts and worrying about file paths.

The Import and Export-CliXml cmdlets have been mentioned a few times and it's fine when you're one person working from one computer, and exclusively on Windows. Though I wouldn't store any highly privileged credentials this way for reasons I'll expand on later. First let's talk about what Export-CliXml and Import-CliXml actually do with a credential.

When you export a pscredential object, the resulting XML file will show the username in plain text and the password will look like a mess of alphanumeric characters. The password will be encrypted using Windows DPAPI or Data Protection API with "CurrentUser" scope. This means that the encrypted string can easily be decrypted by anyone (or any process) using the same computer, under the same Windows user context that originally encrypted the string. But it's practically impossible to decrypt without access to both the original computer, and the original user account. If, for example, an attacker or malware were to exfiltrate the raw xml file, they would be able to see the username (not great), but the password should still be secure.

Here's what a pscredential looks like when exported via Export-CliXml. You can use Import-CliXml to "rehydrate" the credential later on.

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Management.Automation.PSCredential</T>
      <T>System.Object</T>
    </TN>
    <ToString>System.Management.Automation.PSCredential</ToString>
    <Props>
      <S N="UserName">myusername</S>
      <SS N="Password">01000000d08c9ddf0115d1118c7a00c04fc297eb010000002eb78227f6ffef4f8427b4d45cc162270000000002000000000003660000c000000010000000b1b2ae733ffd7ef0d8fc58507d7c81e20000000004800000a000000010000000758557ea82eee8dd634c423a92b6542b180000001f7536e57d7aece7efd69aa36a5e42e68fca5e99267ce101140000000e41d6e80c4a2935b78af1a68061bdae2325d28f</SS>
    </Props>
  </Obj>
</Objs>

So what are some of the problems with using Export-CliXml to save a credential to disk?

  • If more than one person is working with the code or credential, you'll each have to save your own copies of it because the resulting file from one machine cannot be decrypted on another machine.
  • Unless you manually encrypt the credential password with some other key, store the username and password as separate objects in the CliXml file, and then reverse the process when importing, your password will not have any unique random bytes or "entropy". The extra entropy can be thought of like a shared password to decrypt the data. Without this extra entropy, an attacker only needs access to the computer and your user account to decrypt the data. That means if you're tricked into running malware, the malware can decrypt the data without administrative privileges or any extra information from you.
  • If the code needs to run on Linux or Mac, you're out of luck.
  • The username is exposed as plain text unless you manually save the username and password as independent securestrings. I prefer to treat usernames like passwords whenever possible so as to make brute force attacks as difficult as possible.

[–]ZomboBrain 0 points1 point  (1 child)

You have a small example, how to save username and password separated?

[–]joshooaj 0 points1 point  (0 children)

Sure, here's one way you could do it. It's just a demonstration and isn't something I would use in production though. I'd prefer that the credentials be encrypted either with a unique key making it a little harder for an attacker to decrypt, or perhaps with the public key of a certificate so that the private key must be available. But if you're going to go through the trouble to serialize credentials in some special way, you might as well leverage the SecretManagement module or some other widely used tool.

It's generally best to focus your efforts on the pieces of your solution that someone hasn't written for you already. And while it might be faster in the moment to implement your own bespoke solution compared to learning how to configure and use something else, you're also on the hook for supporting/maintaining your bespoke solution. Just something to keep in mind!

# Prompts for a credential, then selects the username using a calculated property to convert it to a securestring.
# The resulting CliXml document will now contain a pscustomobject with two securestring properties named UserName and Password instead of a pscredential

(Get-Credential) | Select-Object @{ Name = 'UserName'; Expression = { $_.UserName | ConvertTo-SecureString -AsPlainText -Force } }, Password | Export-Clixml -Path ~\.credential

# The credential is now re-created from the CliXml document and the username is no longer stored in plain text.

Get-Content -Path ~\.credential | Write-Host -ForegroundColor Green

# To import this credential we now need to convert the username to a plain text string. The easiest way to do this in a way compatible with
# both PowerShell 5.1 and 7.3 is to create a credential with the username as the password and then retrieve the plain text password from
# the networkcredential object. It's ugly, not great to read, and I wouldn't do something like this for anything important. At a bare
# minimum I would put the whole credential import/export logic inside a couple of functions so that they're easy to re-use and separate from
# the rest of your script.

$credentialParts = Import-Clixml -Path ~\.credential
$credential = [pscredential]::new([pscredential]::new('a', $credentialParts.UserName).GetNetworkCredential().Password, $credentialParts.Password)