What Blue Team needs to know about Run Script feature in Azure

Run Script is great feature that help cloud system admin perform command or script execution on target virtual machine without RDP or setting up a PsRemote that may not be allowed in your organization. Nonetheless Run Script also allows bad actor to perform a malicious command if he has enough permission. That would become worst if the malicious execution is succeeded.

As a Blue teamer working on Azure, there should be a deep understanding of how Run Script works as well as how to detect and trace what were run on a compromised virtual machine.

Feature Overview

The very first thing we normally do when trying to understand something is to exercise. Specific to this case, we need to perform a Run Script on a virtual machine and observe it. In Azure virtual machine, you can use Azure Portal to execute a script or command. Microsoft provides 10 built-in scripts (except RunPowerShellScrit is just an editor to author a script).

When you click on any of built-in scripts you can see the script that is made. For example take a look at EnableRemotePS which is kind of interesting:

Enable-PSRemoting -Force
New-NetFirewallRule -Name "Allow WinRM HTTPS" -DisplayName "WinRM HTTPS" -Enabled True -Profile Any -Action Allow -Direction Inbound -LocalPort 5986 -Protocol TCP
$thumbprint = (New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My).Thumbprint
$command = "winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname=""$env:computername""; CertificateThumbprint=""$thumbprint""}"
cmd.exe /C $command

You can execute your own custom script by using RunPowerShellScript. Let’s try this one below to get OS information of the virtual machine.

Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion

Not only Azure Portal, you can use Azure CLI or PowerShell to run a script. Let’s say you want to run a script below on a virtual machine:

    [Parameter(Mandatory = $true)]

    [Parameter(Mandatory = $true)]

Write-Host $Message "from" $From

To run this script using Azure PowerShell module, use Invoke-AzVMRunCommand  as follows:

$rgName = "secops-rg"
$id = "RunPowerShellScript"
$scriptPath = ".\helloword.ps1"
$vmName = "w2016-vm"
$vm = Get-AzVm -ResourceGroupName $rgName `
               -Name $vmName

Invoke-AzVMRunCommand -ResourceGroupName $rgName `
                      -Name $vm.Name `
                      -CommandId $id `
                      -ScriptPath $scriptPath `
                      -Parameter @{"Message" = "HelloWord";
                                   "From" = "AzSec" }

Once the script execution is succeeded the Message field gives you the script output.

Deeper Understanding

So far you know how to run a script remotely. You may check VM Extension to from Azure Portal to see if there is one. Unfortunately Run Script doesn’t actually create an extension. All stuffs that come with Run Script on Windows virtual machine are stored in C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\{version}.

For Linux you can find Run Script extension, configuration in /var/lib/waagent/run-command

There are two things that are involved entirely from triggering a Run Script API to executing that script:

  • Command Invocation
  • Script Execution

Command Invocation

Because we don’t know what API does behind the scene so we could only dig into PowerShell module DLL. We can find module path by simple command below:

(Get-Module -ListAvailable Az.Compute*).path

There are two DLLs that we would need to dig into is Microsoft.Azure.Management.Compute.dll and Microsoft.Azure.PowerShell.Cmdlets.Compute.dll . There are free decompilers you can use. I use JetBrains dotPeek to dig into DLL in Windows. There are a lot of classes in this DLL. However I’d only pay attention to Microsoft.Azure.Commands.Compute.Automation  . There is a class named InvokeAzureRmVMRunCommand  . In this command, there is a code snippet that catches my attention:

if (this.ScriptPath != null)
    parameters.Script = (IList<string>) new List<string>();
    string str = FileUtilities.get_DataStore().ReadFileAsText(new FileInfo(((AzurePSCmdlet) this).get_SessionState().Path.GetUnresolvedProviderPathFromPSPath(this.ScriptPath)).FullName);
    parameters.Script = (IList<string>) str.Split(new string[3]
    }, StringSplitOptions.RemoveEmptyEntries);

This code simply checks null in ScriptPath parameter and then read the file and split the script with ReadFileAsText()  then use Split()  function with \r  and \n to split the script string. So the friendly format of the above HelloWord example becomes the following

Param(","    [Parameter(Mandatory = $true)]","    [string]","    $Message,","    [Parameter(Mandatory = $true)]","    [string]","    $From",")","Write-Host $Message \"from\" $From"

Now we have an idea about your script content. Next step is to find out where script content is stored before it is sent along with request body to Azure REST API. Normally there is a client object that is initialized. Specific to Azure Computer, it is ComputerManagementClient . During the search in this object, I found an initiation:

this.VirtualMachineRunCommands = (IVirtualMachineRunCommandsOperations) new VirtualMachineRunCommandsOperations(this);

The next step is to locate VirtualMachineRunCommandsOperations() as well as request body in a HTTP Request made to Azure API. From the search, I found an object named RunCommandDocumentBase  that is a JSON object.

[JsonProperty(PropertyName = "script")]
public IList<string> Script { get; set; }

[JsonProperty(PropertyName = "parameters")]
public IList<RunCommandParameterDefinition> Parameters { get; set; }

You may find more useful information of Run Command/Run Script feature. At this point, we know that the request body contains a long string , and parameter to be used.

Script Execution

Most of the features that interact with Azure virtual machine Microsoft provides an extension – they call it extension. And they are dropped to the path C:\Packages\Plugins and extension log is at C:\WindowsAzure\Logs\Plugins .You may have known common extensions like:

  • Microsoft.Azure.Diagnostics.IaaSDiagnostics
  • Microsoft.Azure.Security.IaaSAntimalware
  • Microsoft.Compute.VMAccessAgent
  • Microsoft.EnterpriseCloud.Monitoring.MicrosoftMonitoringAgent

Specific to Run Script feature, the extension is called Microsoft.CPlat.Core.RunCommandWindows. The latest version is 1.1.3 (as of this article). Checking directories and files in C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\1.1.3 there are three places that ask for attention:

  • bin
  • Downloads
  • RuntimeSettings

In bin there are some DLLs as well as an application named RunCommandExtension.exe that we should look into. Again, to do the work I use dotPeek. Luckily this application can be decompiled

There are 4 namespaces here. I checked around and noticed a class named Constants under Microsoft.Azure.ComputeResourceProvider.Extensions.RunCommandExtension

    public const string ScriptFileNameTemplate = "script{0}.ps1";
    public const string HandlerEnvironmentFile = "HandlerEnvironment.json";
    public const string ConfigurationFileSuffix = ".settings";
    public const string HandlerLogFile = "RunCommandExtension.log";
    public const string StatusFileSuffix = ".status";
    public const string StatusName = "RunCommandExtension";
    public const string RegKey = "SOFTWARE\\Microsoft\\Windows Azure\\RunCommandExtension";
    public const string EnabledRegKeyValueName = "IsEnabled";
    public const string MostRecentSeqNumStartedRegKeyValueName = "MreSeqNumStarted";
    public const string MostRecentSeqNumFinishedRegKeyValueName = "MreSeqNumFinished";
    public const string UpdatingRegKeyValueName = "IsUpdating";
    public const int NumberOfScriptFiles = 100;
    public const int StatusFileTouchingPeriod = 60;
    public const int MaxStatusFileModifyRetryAttempts = 3;
    public const int NumMostRecentCharsToEmit = 4096;
    public const int FileDownloadRetryInterval = 15;
    public const int MaxPerFileDownloadRetryTime = 600;
    public const int MaxFileDownloadExecutionTime = 30;
    public const int LogFileWriteRetryAttempts = 3;
    public const int LogFileWriteRetryTime = 500;
    public const int ExitCode_Okay = 0;
    public const int ExitCode_HandlerFailed = -1;
    public const int ExitCode_MissingConfig = -2;
    public const int ExitCode_CommandExecutionFailed = -3;
    public const int ExitCode_BadConfig = -4;

This one is interesting. This gives you an idea on what RunCommandExtension application does and what are being set. We know that script file name follows the pattern script{0}.ps1 . Configuration file is stored in a file that has extension *.settings . You may see a constant named NumberOfScriptFiles  which tells the maximum scripts you may run once at a time is 100.

The main program()  is in the Program  class. There are bunch of helpful information that uncover a lot of things about RunCommandExtension application. We see registry key at path HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Azure\RunCommandExtension  is set. We can also see the argument/command that are supported in RunCommandExtension. Each argument is checked to be handled by a handler. You can simply jump to a handler for checking what it does e.g. InstallHandler()

The next one is protectedSetting path, I’d like to keep it for another article coming. In a nutshell, this part stores a signing key that the application uses to decrypt a secret to run the your script.

The next one is command that RunCommandExtension invokes to run. It is straightforward as follows

string commandToExecute = Program.PowerShellPath + Program.ExecutionPolicy + " -File " + path2;
if (protectedSettings2 != null && protectedSettings2.Parameters != null && protectedSettings2.Parameters.Count > 0)
    StringBuilder parameters = new StringBuilder();
    protectedSettings2.Parameters.ForEach((Action<ParameterDefinition>) (p => parameters.Append(string.Format(" -{0} {1}", (object) p.Name, (object) p.Value))));
    commandToExecute += parameters.ToString();

It looks like that the command to be executed is

powershell.exe -ExecutionPolicy Unrestricted -File {path2}

Alright you gotta have an idea now. The application finds a script in Download directory and execute it by invoking PowerShell script. But you may be wondering how that script file is created? Did something pull script from somewhere in the cloud and wrote down to the local file? While I still cannot still figure out the part that the agent downloads a setting file from Azure back-end, I found a part that the RunCommandExtension application reads settings and write the script to a local script file.

string path2 = string.Format("script{0}.ps1", (object) (num % 100L));
string downloadDir = "Downloads\\";
if (publicSettings != null && publicSettings.Script != null && publicSettings.Script.Any<string>())
    string contents = string.Join<string>("\r\n", (IEnumerable<string>) publicSettings.Script);
        if (!Directory.Exists(downloadDir))
        File.WriteAllText(Path.Combine(downloadDir, path2), contents);

Until now we know something that is much brighter and rather than just simply Run Script allows you to execute a script on Azure virtual machine.  To ensure this is a true, you can check files in Downloads and RuntimeSettings directory. Sample setting file is as follows:

    "runtimeSettings": [
            "handlerSettings": {
                "protectedSettingsCertThumbprint": "REDACTED",
                "protectedSettings": "REDACTED",
                "publicSettings": {
                    "script": [
                        "    [Parameter(Mandatory = $true)]",
                        "    [string]",
                        "    $Message,",
                        "    [Parameter(Mandatory = $true)]",
                        "    [string]",
                        "    $From",
                        "Write-Host $Message \"from\" $From"

Here is the flow diagram that illustrates how Run Command/Run Script works:

Run Script Logging

As being part of Blue Team, if something badly happens you would normally ask what left in the system that supports investigation and incident report. First, if you can access to the virtual machine you can find Run Command/Run Script extension log in this path C:\WindowsAzure\Logs\Plugins\Microsoft.CPlat.Core.RunCommandWindows\1.1.3. The file named RunCommandExtension.txt stores history including setting and script that was used.

In the case that you cannot access to check, technically you can still run another script using Run Command/Run Script feature to check:

$files = Get-ChildItem C:\Packages\Plugins\Microsoft.CPlat.Core.RunCommandWindows\1.1.3\Downloads
foreach ($file in $files)
    Write-Host -ForegroundColor Green "[-] START READING FILE"
    Get-Content -Path $file.PSPath
    Write-Host -ForegroundColor Green "[-] END READING FILE:" $file.Name

You may hit the limitation of 4096 bytes output or outbound traffic on port 443 to Azure Public IP (when an incident occurs traffic normally routes to nowhere). Refer to this article for limitations.

With cloud-level logging, Azure logs the operation in Azure Activity Log.

If you write Azure Activity Log to a Log Analytics workspace, you can query:

| where OperationNameValue contains "Microsoft.Compute/virtualMachines/runCommand"
| project TimeGenerated,

If your virtual machine is covered  by Azure Security Center then you have data in SecurityEvent table. Do the query below to see if there is a Run Script activity:

| where CommandLine contains "powershell -ExecutionPolicy Unrestricted"  
| project TimeGenerated,

As of this article, script content is not present in Azure Activity Log or anywhere else so you must capture script binaries on the virtual machine. Depending on how critical and policy you may have, should you have to block Run Command feature or write things to a Log Analyics workspace or your SIEM.

If there is a breach and you cannot get back to desired state of the virtual machine you would have to dump its memory and perform forensic.

Alert and Detection

In a large environment Contributor is normally granted to developer and system admin which includes RunCommand role

  • Microsoft.Compute/locations/runCommands/read
  • Microsoft.Compute/virtualMachines/runCommand/action
  • Microsoft.Compute/virtualMachineScaleSets/virtualMachines/runCommand/action

From Azure Monitor Alert or Azure Sentinel, you can create a query like

| where OperationNameValue contains "Microsoft.Compute/virtualMachines/runCommand"


SecurityEvent | where CommandLine contains "powershell -ExecutionPolicy Unrestricted"

or combine both to increase fidelity

let runCmds = 
| where OperationNameValue contains "Microsoft.Compute/virtualMachines/runCommand"
| distinct _ResourceId;
| where _ResourceId  in (runCmds)
| where CommandLine contains "powershell -ExecutionPolicy Unrestricted" and
        ParentProcessName contains "RunCommandExtension" 
| project TimeGenerated,


Run Command and Run Script are cool features for SysOps guys even SecOps guys (CVE verification). It could be abused by bad actor and if you don’t monitor it you may lose visibility in security monitoring.

This article shows you the Windows part only but what Run Command and Script run in Linux is not much different from Windows. You will see things like setting file, decryption path as well as Run Command utility.

To me personally I wouldn’t recommend Run Command and Run Script. There are good management tools out there providing much faster remote communication and execution. It is good for quick and simple audit though.

Finally, here are a few articles to check:

If you find something not correct or would like to add more feedback, please feel free to leave a comment.

This entry was posted in Monitoring & Detection and tagged , . Bookmark the permalink.

2 Responses to What Blue Team needs to know about Run Script feature in Azure

  1. olivera56 says:

    Good work! Thanks for your time doing detailed analysis

  2. Pingback: Harvest credential from Custom Script Extension on Azure VM -Microsoft Azure Security Randomness

Leave a Reply

Your email address will not be published.