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.
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.
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 }
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=admini[email protected]" -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.