Solving the Dark Faction’s PowerShell Transport Challenge

Before the most recent PowerShell + DevOps Global Summit, the Dark Faction issued a second challenge. Certainly, working code that produces the desired results is important. But equally important is how you might approach this problem. What tools or techniques might you consider? How you work is as important to the Dark Faction as your final code. The Chairman has had his own Iron Scripters working on the challenge and this is one way the problem might be attacked.

Define the Command and Parameters

The first step is to define the code that should be security transported and executed on a different computer. For the sake of this article, we will use this scriptblock.

$sb = {
    Param ([string]$Path='.\data.json', [string]$Encoding='ASCII')

    $h = [pscustomobject]@{
        Volumes      = Get-Volume | Where-Object DriveLetter | Select-object DriveLetter, FileSystemLabel,
        @{Name = "SizeGB"; Expression = { $_.size / 1GB -as [int] } },
        @{Name = "FreeGB"; Expression = { $_.Sizeremaining / 1GB } }
        Processors   = [System.Environment]::ProcessorCount
        TotalMemory  = ((Get-CimInstance -ClassName Win32_ComputerSystem).totalphysicalmemory / 1GB) -as [int]
        Computername = [System.Environment]::MachineName
        Date         = (Get-Date -Format u)
    $h | ConvertTo-Json -Depth 1 | Set-Content -path $path -Encoding $encoding

Nothing too exciting. The challenge is to securely transport this over open networks. To simplify the process, we can convert the scriptblock, which is really just a special type of string into a base64 encoded string.

$bytes = [system.text.encoding]::ASCII.GetBytes($sb)
$enc = [convert]::toBase64string($bytes)

The scriptblock now looks like this.

A PowerShell scriptblock encoded as base64

The challenge was to also include a set of parameter values.

$params = @{Path = ".\result.json"; Encoding = "unicode" }

You can even test the scriptblock and params by splatting the latter.

Splatting parameters to the PowerShell scriptblock

OK so far?

Create an Object to Encode

While not a requirement, it was suggested in the challenge to include some metadata such as the original author and location.

$meta = [ordered]@{
    Computername = [System.Environment]::MachineName
    User         = "$([system.Environment]::userdomainname)\$([system.environment]::username)"
    Date         = (Get-Date).ToUniversalTime()

This code is using the .NET Framework directly with an eye towards cross-platform compatibility. Normally referencing items from the ENV: PSDrive should suffice but with the current version of PowerShell Core this isn’t always an option. Evertything created thus far can be turned into an object.

$toEncode = [pscustomobject]@{
    Metadata    = $meta
    Scriptblock = $enc
    Params      = $params

The object to transport

This is the object that needs to be securely transported. Let’s serialize this object to a JSON file.

$content = $toEncode | ConvertTo-Json

Now for the fun part. This content can be protected as a CMS message using a document encryption certificate installed locally.

protect-cmsmessage -to "[email protected]" -content ($content) -OutFile .\outmsg.txt

This is the file that can be transported.

-----BEGIN CMS-----
-----END CMS-----

Unwrapping the File

At the other end, the process needs to be undone to unwrap and decode the contents into the original PowerShell command and parameters. Assuming the other party has a copy of the document encryption certificate installed, they can unprotect the document, convert the base64 string back into scriptblock and extract the parameters.

$in = Unprotect-CmsMessage -Path .\outmsg.txt | Convertfrom-json
$outbytes = [convert]::FromBase64String($in.Scriptblock)
$cmd = [scriptblock]::create([system.text.encoding]::ASCII.GetString($outbytes))
$ | foreach-object -Begin { $cmdParams = @{ } } -Process {
    $cmdParams.Add($, $_.value)

They can look at the code in $cmd or run with the parameters.

&$cmd @cmdparams

Shipping the Certificate

However, what if the document encryption certificate was not installed on the other end? Here’s one way that might be handled. First, the certificate is exported to a pfx file and password protected.

$pass = Read-Host "Enter a password to protect the certificate" -AsSecureString
Get-ChildItem Cert:\CurrentUser\My\0E35FC5DD64D28AE691D349708BA038B72A164B8 |
Export-PfxCertificate -Password $pass -FilePath .\company-doccert.pfx

The password will eventually need to be securely communicated between parties. We’ll assume the Dark Faction has existing mechanisms.

Because the Dark Faction is a bit paranoid, we can encode the certificate file further using the certutil.exe command line utility.

certutil -encode .\company-doccert.pfx .\company-enc.txt

This file, because it is all text, is easier to transport along with the PowerShell command.

$transport = [pscustomobject]@{
    Cert = (Get-Content .\company-enc.txt -Raw)
    Cmd  = (Get-Content .\outmsg.txt -Raw)

$transport | ConvertTo-Json -Depth 1 | Set-Content -Path .\transport.json

Decoding and Executing

At the other end, the transport.json file can be brought back into PowerShell.

$t = Get-Content .\transport.json | ConvertFrom-Json

The encoded certificate is in $t.cert.value and the code is in $t.cmd.value. Using certutil.exe again, the certificate can be recreated and imported.

if ($t.cert.value) {
    #create a temporary file
    $t.cert.value | Out-File .\tmpcert.txt
    certutil -decode .\tmpcert.txt .\importcert.pfx
    $pass = Read-Host "Enter the password to unprotect the certificate" -AsSecureString
    Import-PfxCertificate -Password $pass -FilePath .\importcert.pfx -CertStoreLocation Cert:CurrentUser\My

With the certificate in place, decoding and executing the rest is as we did above.

$in = $t.cmd.value | Unprotect-CmsMessage | ConvertFrom-Json
$outbytes = [convert]::FromBase64String($in.Scriptblock)
$cmd = [scriptblock]::create([system.text.encoding]::ASCII.GetString($outbytes))
$ | foreach-object -Begin { $cmdParams = @{ } } -Process {
    $cmdParams.Add($, $_.value)

&$cmd @cmdparams


Naturally, to make this a seamless process you want to create a set of reusable commands, most likely packaged as a module. You might have commands to easily encode any PowerShell command expression and parameters, with an option to include the document encryption certificate. And of course, you would want an easy set of commands to consume the encoded file. The Chairman will leave these exercises to you.