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
    $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 "CN=administrators@company.pri" -content ($content) -OutFile .\outmsg.txt

This is the file that can be transported.

-----BEGIN CMS-----
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
-----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))
$in.Params.psobject.properties | foreach-object -Begin { $cmdParams = @{ } } -Process {
    $cmdParams.Add($_.name, $_.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))
$in.Params.psobject.properties | foreach-object -Begin { $cmdParams = @{ } } -Process {
    $cmdParams.Add($_.name, $_.value)
}
&$cmd @cmdparams

Conclusion

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.