Secure Remote PowerShell without PowerShell Remoting or a VPN
As easy as PowerShell is to use, it can be difficult to scale beyond a local Active Directory domain. Sure you could set up VPNs, but even those can be difficult and require special software and/or hardware. Using BrazenCloud allows you to execute remote commands on any system located anywhere as long as the target has the BrazenCloud Agent installed. And the really cool part is that BrazenCloud can execute this very quickly.
Learn more About BrazenCloud’s Features
Learn more about BrazenCloud’s Technology
Prerequisites
- Windows PowerShell 5.1 or PowerShell 6+
- A BrazenCloud Platform Account
- At least 1 BrazenCloud Agent configured
- The BrazenCloud PowerShell SDK v0.1.3 or higher
Video
Usage
First, make sure that you have authenticated. After running this command you will be prompted for your BrazenCloud Platform password:
Connect-Runway -Email <emailaddress>
Then you can check for your runner using Get-RwRunner:
(Get-RwRunner).Items
The output of which, will look similar to:
With the name of the BrazenCloud Agent, we can now use the Invoke-RwPowerShellCommand
to execute a PowerShell command on the remote agent. For instance, we can return the local users on a remote server:
$users = Invoke-RwPowerShellCommand -RunnerName 'winserver-UAE' -ScriptBlock {Get-LocalUser}
The command will run the Get-LocalUser
PowerShell command on the BrazenCloud Agent called winserver-UAE
.
The output of the command is returned as a PowerShell object, which is so cool! This allows you to run a remote command and then work with the data locally as you would with any other PowerShell cmdlet.
Here is what the results look like:
For reference, in our production BrazenCloud Platform environment talking to a BrazenCloud Agent on a Windows server hosted in the UAE North Azure region (chosen because it is physically distant from us), the results were measured using the following command:
Measure-Command {Invoke-RwPowerShellCommand -RunnerName 'winserver-UAE' -ScriptBlock {Get-LocalUser}}
The command takes an average of around 15 seconds.
Be aware that the first time the command is run, the BrazenCloud Agent caches the Action that the command depends on. This adds a few seconds depending on the internet connection.
Explanation
Now this isn't some magical, black box software. Here at BrazenCloud, we pride ourselves in making a lot of our platform open. Besides the SDK itself, the Invoke-RwPowerShellCommand
command depends on 2 things:
- The
Invoke-RwPowerShellCommand
command available in the BrazenCloud PowerShell SDK. - The
powershell:runcommand
Action available in BrazenCloud.
Here's a quick diagram of how these pieces interact:
If you are not familiar with Runway Actions, be sure to check out the BrazenCloud documentation on Actions .
Invoke-RwPowerShellCommand
If you are interested, you can find the source code for the Invoke-RwPowerShellCommand
command in the
BrazenCloud PowerShell SDK repository
.
The command relies on the PowerShell SDK to create a Job, wait for it to complete, and then serialize the output into a PowerShell object.
Parameters
RunnerName
: The name of the BrazenCloud Agent to run the command on.RunnerId
: The ID of the Runner to run the command on.ScriptBlock
: The command to run on the Runner.PWSH
: If passed, the Action will attempt to execute in PowerShell 6+.LeaveJob
: If passed, the command will leave the Job in your Runway tenant's Job history. Since the command is designed to be ephemeral, the job is deleted upon completion by default. This is useful for troubleshooting.SerializeDepth
: When complex objects are returned, this is how deep the object should be serialized. Similar to theDepth
parameter onConvertTo-Json
.DefaultPropertiesOnly
: If passed, the Action will only serialize the default properties for the object that is generated. For example, if runningGet-Service
with this parameter, only Status, Name, and DisplayName would be returned. This can greatly improve performance depending on the command.
Creating a job
Before we create the job, we'll need to look up the ID of the powershell:runcommand
Action:
$runCommand = Import-RwRepository -Name 'PowerShell:RunCommand'
To create a Job, we first need to create a 'set', which is a group of devices that we can use to assign to a Job, and then add our Runner to it:
$assignSet = New-RwSet
Add-RwSetToSet -TargetSetId $assignSet -ObjectIds $RunnerId | Out-Null
Then, if we want to use the random Job name generator, we can do so by making a raw API call since that command in the SDK is currently bugged:
$jobName = (Invoke-RestMethod -Headers @{Authorization = "Session $($env:RunwaySessionToken)"} -Uri 'https://portal.runway.host/api/v2/jobs/name' -Method Get)
The BrazenCloudSessionToken environment variable is automatically created when you run Connect-Runway
Then, we can create the job, passing the parameters to the Action. Many of the variables being passed to the Job come from the function parameters:
$nj = New-RwJob -IsEnabled -IsHidden:$false -EndpointSetId $assignSet -Name $jobName -ScheduleType 'RunNow' -Actions @(
@{
RepositoryActionId = $runCommand.Id
Settings = @{
Command = $ScriptBlock
PWSH = $PWSH.IsPresent
'Serialize Depth' = $SerializeDepth
'Default Properties Only' = $DefaultPropertiesOnly.IsPresent
'Debug' = $true
}
}
)
Once the job has been created, the command loops while waiting for the job to complete:
$job = Import-RwJob -JobId $nj.JobId
While($job.TotalEndpointsFinished -lt $job.TotalEndpointsAssigned) {
Start-Sleep -Seconds 2
$job = Import-RwJob -JobId $nj.JobId
}
Once the job has completed, we have to look up the Thread ID sa that we can read the thread log, which is any console output from the Action. The log, which, in this case, is a CliXml deserialized PowerShell object, is then written to disk, and imported with Import-CliXml
:
$completedRunner = (Invoke-RwQueryEndpointAsset -RootContainerId $assignSet -MembershipCheckId $nj.JobId -IncludeSubgroups -Skip 0 -Take 20 -SortDirection 0).Items
Get-RwJobThreadLastLog -ThreadId $completedRunner.LastThreadId -OutFile .\rwtmp.txt
Get-Content .\rwtmp.txt | Where-Object {$_ -notlike '# *'} | Out-File .\results.xml -Force
Import-CliXml .\results.xml
Since we are using CliXml, we get a usable PowerShell object, just like with Invoke-Command
.
powershell:runcommand
If you are interested, you can find the source code for the powershell:runcommand
Action in the
Runway Actions repository
.
To get a complete understanding of how to develop Actions for BrazenCloud, be sure to refer to our Action Developer documentation .
The powershell:runcommand
Action is the part of this process that runs on the BrazenCloud Agent. It receives the command and parameters sent to the Job by the Invoke-RwPowerShellCommand
command, and executes them accordingly.
First, we load the
settings.json file
and write some debug output, which is useful when testing. You'll notice that each line is prepended with a #
. Invoke-RwPowerShellCommand
ignores those lines when it serializes the return:
$json = Get-Content .\settings.json
$settings = $json | ConvertFrom-Json
if ($settings.Debug.ToString() -eq 'true') {
$json.Split("`n") | Foreach-Object {"# $_"}
}
if ($settings.Debug.ToString() -eq 'true') {
"# $($PSVersionTable.PSVersion.ToString())"
}
Then, if PWSH
is passed, we attempt to call the script in PowerShell:
if ($settings.'PWSH' -eq $true -and $PSVersionTable.PSVersion.Major -le 5) {
try {
pwsh -command {exit}
} catch {
Write-Host 'PWSH is not installed. Cannot complete.'
exit
}
pwsh -ExecutionPolicy Bypass -File $MyInvocation.MyCommand.Path
}
Lastly, we'll actually run the command. By first converting it to a script block:
$sb = [scriptblock]::Create($settings.Command)
And then, if the Raw Output
parameter is passed, we'll simply return the output of the command as a plain old string:
if ($settings.'Raw Output'.ToString() -eq 'true') {
Invoke-Command -ScriptBlock $sb
}
If that parameter is not passed, we'll plan on deserializing the output as CliXML. But first, we'll run the command, stash it in a variable, and filter the output if Default Properties Only
was passed:
$output = Invoke-Command -ScriptBlock $sb
$select = '*'
if ($settings.'Default Properties Only' -eq $true) {
if ($output.GetType().BaseType.Name -eq 'Array') {
if ($output[0].PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames) {
$select = $output[0].PSStandardMembers.DefaultDisplayPropertySet.ReferencedPropertyNames
}
}
}
And lastly, we'll export the object to CliXml, using the Serialize Depth
parameter, and then read the CliXml file so that it is written to the console:
$output | Select-Object $select | Export-Clixml -Depth $settings.'Serialize Depth' .\results\output.clixml
Get-Content .\results\output.clixml
Have some feedack?
If you are giving this a try and run into any issues or maybe you've thought of a killer feature that you'd like to see, please open up an issue in our PowerShell SDK Github repository. The SDK is actively being developed, so any questions, bugs, or feature requests will be addressed very quickly.
A parting note
The last thing to be aware of is that as an automation platform, BrazenCloud is incredibly flexible. Sure we demonstrate an awesome PowerShell process here (and I love PowerShell), but Runway is designed to run arbitrary code. If you have a script or executable that you can run locally, you can scale it with Runway. If you want to give Runway a try, create a free account . You don't even have to talk to a salesperson to get started.