BrazenCloud

View Original

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

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:

Output of Get-RwRunner

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:

Output of Invoke-RwPowerShellCommand

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:

Invoke-RwPowerShellCommand overview

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 the Depth parameter on ConvertTo-Json.
  • DefaultPropertiesOnly: If passed, the Action will only serialize the default properties for the object that is generated. For example, if running Get-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.