Links

Arsenal Aggressor Script

Aggressor script to automatically download and load an arsenal of open source and private tooling. Hopefully, this saves other teams time and helps the community!

TL;DR

Repository with final scripts can be found here.

Introduction

A red team often spends a lot of time and effort on collecting, developing, building and updating toolsuites. Furthermore, it is challenging to distribute everything securely and ensure everyone in the team has access to the same toolkits. In this article, we will discuss how you can create one Cobalt Strike aggressor script, which pulls in the latest releases of your tooling. This warrants that every red team operator has the same toolkit at the beginning of every red team operation.
Please note that although our approach is implemented with an internal Gitlab, the examples in this article were changed to use public Github repositories instead of our own private instance to make the proof of concept easy to reproduce. Keep in mind that pulling in public tooling without review is considered bad practice and should be avoided.
That said, let's begin our journey!

Sleep

Cobalt Strike agressor scripts are written in Sleep. This obscure Perl-like Java scripting language allows you to customise many components of Cobalt Strike, resulting in increased stealth or additional functionality/quality of life improvements. The Cobalt Strike Community Kit is a great example of what can be accomplished.
The manual of Sleep can be found here.

Download Artifacts via Aggressor Script

The first challenge was to download the latest build.zip releases from Gitlab via Aggressor script in the Cobalt Strike client. Luckily, @mariuszbit already came up with the following awesome implementation, making use of the interoperability between Sleep scripting and Java. The script effectively enables the Cobalt Strike client to send HTTP requests, making it perfect to interact with services like GitHub (or private Gitlab).
Let's create a script that downloads the zip of Trustedsec's popular Remote Ops BOF and Situational Awareness BOF repositories. Both projects are awesome examples of helpful open source tooling to extend Cobalt Strike, without increasing detection. The following script is based on the work of our intern, Lucas Cornette, and mgeeky's httprequest implementation.
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================
# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load
@repositories = @(
@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
# add other entries here as well
);
# ============================
# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
for($i = 0; $i < size(@repositories); $i++) {
downloadRepo(@repositories[$i][1],@repositories[$i][0]);
}
}
# createDir($path, $type);
sub createDir {
$path = $1;
$type = $2;
mkdir($path)
if (checkError($error)) {
warn("Unable to create $type directory: $error");
$eMsg = "true";
}
}
# createFile($path);
sub createFile {
$path = $1;
createNewFile($path);
}
# writeFile($handle, $path);
sub writeFile {
$handle = $1;
$path = $2;
$outFile = openf(">" . $path);
writeb($outFile, readb($handle, -1));
closef($outFile);
}
# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
$downloadUrl = $1;
$fileName = $2 . ".zip";
# Create downloads dir if it does not exist
if (!-exists $saveFilePath) {
createDir($saveFilePath);
}
println("> Downloading " . $fileName . "...");
println("\tDownload url: $downloadUrl");
# Check if the zip file can be created
$saveFilePathProper = $saveFilePath . "/" . $fileName;
createFile($saveFilePathProper);
# Obtain the content and write to file
$handle = httpGet($downloadUrl);
writeFile($handle, $saveFilePathProper);
}
# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
$method = "GET";
$url = $1;
$n = 0;
if(size(@_) == 2) { $n = $2; }
$maxRedirectsAllowed = 10;
if ($n > $maxRedirectsAllowed) {
warn("Exceeded maximum number of redirects: $method $url ");
return "";
}
$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";
try {
# Build the HTTP Request
$urlobj = [new java.net.URL: $url];
if (checkError($error)) {
warn("1: $error");
}
$con = $null;
$con = [$urlobj openConnection];
[$con setRequestMethod: $method];
[$con setInstanceFollowRedirects: true];
[$con setRequestProperty: "Accept", "*/*"];
[$con setRequestProperty: "Cache-Control", "max-age=0"];
[$con setRequestProperty: "Connection", "keep-alive"];
[$con setRequestProperty: "User-Agent", $USER_AGENT];
# Send the request and obtain the content
$inputstream = [$con getInputStream];
$handle = [SleepUtils getIOHandle: $inputstream, $null];
$responseCode = [$con getResponseCode];
# If a redirect is returned, try again with the redirect target (max 10 times)
if (($responseCode >= 301) && ($responseCode <= 304)) {
warn("Redirect");
$loc = [$con getHeaderField: "Location"];
httpRequest($loc, $n + 1);
}
# return the handle with the content
return $handle;
}
catch $message {
warn("HTTP Request failed: $method $url : $message ");
printAll(getStackTrace());
return "";
}
}
# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
The script can now be loaded via the Cobalt Strike > Script Manager.
Successfully loaded cna script
After loading the script, a new menu appears in the UI.
Arsenal menu in the UI with Download button
Clicking the "Download" button should initiate the download of the Remote Ops BOF and Situational Awareness BOF collections. However, it seems that Java does not trust GitHub and the download fails. Ouch!
Certificate trust issue

Challenge 1: Root CA Trust in Sleep

There are several ways to get around this:
  1. 1.
    Ignore SSL/TLS certificate warnings and force the connection. Have you ever implemented this? This would break trust and would be just as complex to implement in Sleep as any other solution.
  2. 2.
    Find the Java truststore that is used by Cobalt Strike and add GitHub's (or your private GitLab's) root CA to it. This would require you to essentially patch the client jar on each release, which would be very impractical.
  3. 3.
    Dynamically load GitHub's root CA from a base-64 encoded string inline in the cna script, using the interoperability with Java. I gave this a shot at first, but could not figure out how to convert the certificate string to a proper Java byte array in Sleep (dm me on Twitter if you know the answer!).
  4. 4.
    Dynamically load a custom trustore and apply it to the SSLContext of the HTTP request implementation. This was a process of trial and error to get it to work in Sleep, but ultimately the most viable solution. It only requires you to distribute the Arsenal cna script and truststore together in the same directory. This also greatly simplifies the process if you ever have to include another root CA or you need to replace/remove trusted certificates.

Truststore Creation

Adding GitHub's root CA to the truststore is trivial. First, inspect what the name of the CA is by browsing to github.com in e.g. Chrome and inspect the certificate. It appears that DigiCert Global Root is the CA for this website.
DigiCert Global Root CA is the root CA of GitHub
If you are on a mac, you should already have this CA somewhere in your keychain. Simply search for the root CA and export it in the .cer file format. You can simply select the row and do File > Export Items..
DigiCert Global Root CA in keychain
Finally, create the truststore and import the CA.cer file.
keytool -import -alias digicertroot -keystore truststore-public.jks -file /Users/0xbad53c/rootcas/DigiCert\ Global\ Root\ CA.cer
Added root CA to the truststore

Dynamic Truststore Load

The next step is to load the trustore dynamically. This again was a process of trial and error, but I came up with the following approach. The idea was to use the interoperability with Java to load the newly created trustore and apply it to the SSLContext of the HTTP request.
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
...
# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
$method = "GET";
$url = $1;
$n = 0;
if(size(@_) == 2) { $n = $2; }
$maxRedirectsAllowed = 10;
if ($n > $maxRedirectsAllowed) {
warn("Exceeded maximum number of redirects: $method $url ");
return "";
}
$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";
try {
# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
$filepath = script_resource("truststore-public.jks");
# The trust store file and optional password to unlock it
$trustStoreFile = [new File: $filepath];
$trustStorePassword = "changeit";
# Load the trust store from file into an object, the type is "jks"
$trustStore = [KeyStore getInstance: "JKS"];
[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];
# init TrustManagerFactory with truststore containing root CA
$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
[$tmf init: $trustStore];
# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
$sslContext = [SSLContext getInstance: "TLS"];
[$sslContext init: $null, [$tmf getTrustManagers], $null];
[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];
...
This resulted in version two of the script, capable of downloading zips from GitHub.
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================
# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load
@repositories = @(
@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
# add other entries here as well
);
# ============================
# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
for($i = 0; $i < size(@repositories); $i++) {
downloadRepo(@repositories[$i][1],@repositories[$i][0]);
}
}
# createDir($path, $type);
sub createDir {
$path = $1;
$type = $2;
mkdir($path)
if (checkError($error)) {
warn("Unable to create $type directory: $error");
$eMsg = "true";
}
}
# createFile($path);
sub createFile {
$path = $1;
createNewFile($path);
}
# writeFile($handle, $path);
sub writeFile {
$handle = $1;
$path = $2;
$outFile = openf(">" . $path);
writeb($outFile, readb($handle, -1));
closef($outFile);
}
# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
$downloadUrl = $1;
$fileName = $2 . ".zip";
# Create downloads dir if it does not exist
if (!-exists $saveFilePath) {
createDir($saveFilePath);
}
println("> Downloading " . $fileName . "...");
println("\tDownload url: $downloadUrl");
# Check if the zip file can be created
$saveFilePathProper = $saveFilePath . "/" . $fileName;
createFile($saveFilePathProper);
# Obtain the content and write to file
$handle = httpGet($downloadUrl);
writeFile($handle, $saveFilePathProper);
}
# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
$method = "GET";
$url = $1;
$n = 0;
if(size(@_) == 2) { $n = $2; }
$maxRedirectsAllowed = 10;
if ($n > $maxRedirectsAllowed) {
warn("Exceeded maximum number of redirects: $method $url ");
return "";
}
$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";
try {
# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
$filepath = script_resource("truststore-public.jks");
# The trust store file and optional password to unlock it
$trustStoreFile = [new File: $filepath];
$trustStorePassword = "changeit";
# Load the trust store from file into an object, the type is "jks"
$trustStore = [KeyStore getInstance: "JKS"];
[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];
# init TrustManagerFactory with truststore containing root CA
$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
[$tmf init: $trustStore];
# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
$sslContext = [SSLContext getInstance: "TLS"];
[$sslContext init: $null, [$tmf getTrustManagers], $null];
[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];
# Build the HTTP Request
$urlobj = [new java.net.URL: $url];
if (checkError($error)) {
warn("1: $error");
}
$con = $null;
$con = [$urlobj openConnection];
[$con setRequestMethod: $method];
[$con setInstanceFollowRedirects: true];
[$con setRequestProperty: "Accept", "*/*"];
[$con setRequestProperty: "Cache-Control", "max-age=0"];
[$con setRequestProperty: "Connection", "keep-alive"];
[$con setRequestProperty: "User-Agent", $USER_AGENT];
# Send the request and obtain the content
$inputstream = [$con getInputStream];
$handle = [SleepUtils getIOHandle: $inputstream, $null];
$responseCode = [$con getResponseCode];
# If a redirect is returned, try again with the redirect target (max 10 times)
if (($responseCode >= 301) && ($responseCode <= 304)) {
warn("Redirect");
$loc = [$con getHeaderField: "Location"];
httpRequest($loc, $n + 1);
}
# return the handle with the content
return $handle;
}
catch $message {
warn("HTTP Request failed: $method $url : $message ");
printAll(getStackTrace());
return "";
}
}
# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
After loading this version of the script and clicking the Arsenal > Download menu item, the Script Console confirms that the zips were downloaded.
Successful download of zip files
The downloads folder was created and now contains the zip files, as expected.
Repository zip files in the downloads folder of the project directory

Challenge 2: Unzip Repositories

The next step would be to unzip these repositories, so we can load the cna scripts and start using the BOFs. For this proof of concept, we use the prebuilt BOFs included in both repositories. This is a bad practice in production and you should ALWAYS BUILD YOUR OWN FROM A TRUSTED SOURCE. Lucas came up with the following approach to use Sleep and Java to unzip the files.
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
...
# Unzips the downloaded zipfile and puts the folder in the main project directory
# unzip($name, $folderName);
sub unzip {
$zipFileName = $1 . ".zip";
$folderName = $2;
$zipFile = $saveFilePath . "/" . $zipFileName;
$destPathProper = $destPath . "/" . $folderName;
$currDir = cwd();
# load and unzip zipfile
$fis = [new java.io.FileInputStream: $zipFile];
$zis = [new java.util.zip.ZipInputStream: $fis];
if ([$fis available] == 0) {
println("Archive is empty. Aborting.");
[$zis closeEntry];
[$fis close];
exit();
}
#Create and go to the destination directory
createDir($destPathProper, "zip destination");
chdir($destPathProper);
# Write all zip contents to the directory
$entry = [$zis getNextEntry];
while ($entry) {
# Check if $entry is an empty directory
if ([$entry isDirectory]) {
createDir(getFileProper($entry), "empty"); # Create empty directory
}
# Else $entry is a file
else {
$fullPath = getFileProper($destPathProper, $entry);
createDir(getFileProper(getFileParent($entry)), "parent"); # Create the parent directory
createFile($fullPath); # Create the file
$zipHandle = [SleepUtils getIOHandle: $zis, $null];
writeFile($zipHandle, $fullPath);
}
$entry = [$zis getNextEntry];
}
[$zis closeEntry];
[$fis close];
# Change directory back to initial
chdir($currDir);
println("> Files unzipped");
}
This brings us to version three of the script.
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================
# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load
@repositories = @(
@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
# add other entries here as well
);
# ============================
# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
for($i = 0; $i < size(@repositories); $i++) {
downloadRepo(@repositories[$i][1],@repositories[$i][0]);
unzip(@repositories[$i][0], @repositories[$i][0]);
}
}
# createDir($path, $type);
sub createDir {
$path = $1;
$type = $2;
mkdir($path)
if (checkError($error)) {
warn("Unable to create $type directory: $error");
$eMsg = "true";
}
}
# createFile($path);
sub createFile {
$path = $1;
createNewFile($path);
if (checkError($error)) {
warn("Cannot create file $path: $error");
$eMsg = "true";
}
}
# writeFile($handle, $path);
sub writeFile {
$handle = $1;
$path = $2;
$outFile = openf(">" . $path);
writeb($outFile, readb($handle, -1));
closef($outFile);
if (checkError($error)) {
warn("Error while writing to file: $error");
$eMsg = "true";
}
}
# Unzips the downloaded zipfile and puts the folder in the main project directory
# unzip($name, $folderName);
sub unzip {
$zipFileName = $1 . ".zip";
$folderName = $2;
$zipFile = $saveFilePath . "/" . $zipFileName;
$destPathProper = $destPath . "/" . $folderName;
$currDir = cwd();
# load and unzip zipfile
$fis = [new java.io.FileInputStream: $zipFile];
$zis = [new java.util.zip.ZipInputStream: $fis];
if ([$fis available] == 0) {
println("Archive is empty. Aborting.");
[$zis closeEntry];
[$fis close];
exit();
}
#Create and go to the destination directory
createDir($destPathProper, "zip destination");
chdir($destPathProper);
# Write all zip contents to the directory
$entry = [$zis getNextEntry];
while ($entry) {
# Check if $entry is an empty directory
if ([$entry isDirectory]) {
createDir(getFileProper($entry), "empty"); # Create empty directory
}
# Else $entry is a file
else {
$fullPath = getFileProper($destPathProper, $entry);
createDir(getFileProper(getFileParent($entry)), "parent"); # Create the parent directory
createFile($fullPath); # Create the file
$zipHandle = [SleepUtils getIOHandle: $zis, $null];
writeFile($zipHandle, $fullPath);
}
$entry = [$zis getNextEntry];
}
[$zis closeEntry];
[$fis close];
# Change directory back to initial
chdir($currDir);
println("> Files unzipped");
}
# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
$downloadUrl = $1;
$fileName = $2 . ".zip";
# Create downloads dir if it does not exist
if (!-exists $saveFilePath) {
createDir($saveFilePath);
}
println("> Downloading " . $fileName . "...");
println("\tDownload url: $downloadUrl");
# Check if the zip file can be created
$saveFilePathProper = $saveFilePath . "/" . $fileName;
createFile($saveFilePathProper);
# Obtain the content and write to file
$handle = httpGet($downloadUrl);
writeFile($handle, $saveFilePathProper);
}
# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
$method = "GET";
$url = $1;
$n = 0;
if(size(@_) == 2) { $n = $2; }
$maxRedirectsAllowed = 10;
if ($n > $maxRedirectsAllowed) {
warn("Exceeded maximum number of redirects: $method $url ");
return "";
}
$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";
try {
# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
$filepath = script_resource("truststore-public.jks");
# The trust store file and optional password to unlock it
$trustStoreFile = [new File: $filepath];
$trustStorePassword = "changeit";
# Load the trust store from file into an object, the type is "jks"
$trustStore = [KeyStore getInstance: "JKS"];
[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];
# init TrustManagerFactory with truststore containing root CA
$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
[$tmf init: $trustStore];
# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
$sslContext = [SSLContext getInstance: "TLS"];
[$sslContext init: $null, [$tmf getTrustManagers], $null];
[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];
# Build the HTTP Request
$urlobj = [new java.net.URL: $url];
if (checkError($error)) {
warn("1: $error");
}
$con = $null;
$con = [$urlobj openConnection];
[$con setRequestMethod: $method];
[$con setInstanceFollowRedirects: true];
[$con setRequestProperty: "Accept", "*/*"];
[$con setRequestProperty: "Cache-Control", "max-age=0"];
[$con setRequestProperty: "Connection", "keep-alive"];
[$con setRequestProperty: "User-Agent", $USER_AGENT];
# Send the request and obtain the content
$inputstream = [$con getInputStream];
$handle = [SleepUtils getIOHandle: $inputstream, $null];
$responseCode = [$con getResponseCode];
# If a redirect is returned, try again with the redirect target (max 10 times)
if (($responseCode >= 301) && ($responseCode <= 304)) {
warn("Redirect");
$loc = [$con getHeaderField: "Location"];
httpRequest($loc, $n + 1);
}
# return the handle with the content
return $handle;
}
catch $message {
warn("HTTP Request failed: $method $url : $message ");
printAll(getStackTrace());
return "";
}
}
# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
After loading and executing the new script, the script console indicates that files are successfully downloaded and unzipped.
Successful download and unzip
When checking our project folder, it seems to confirm this. Success!
Successful download and unzip of both repositories

Challenge 3: Nested Aggressor Script Load

One of the major challenges we faced in our Cobalt Strike OffSecOps approach was that when loading our "master" script in the UI, included scripts do not show up and cannot be individually unloaded via the default Cobalt Strike functionality.
Another issue was that if script A in the main directory wants to include trustedsec SA.cna in the SA subdirectory, SA.cna would be loaded as if it was loaded from the main directory. This is because SA.cna also uses script_resource() to determine the relative path to the BOF object file, breaking the link. Consider the following example where a script importer.cna loads importee.cna from a subdirectory.

importer.cna

# default Sleep way to load nested script
$cnaScriptPath = script_resource("subdirectory/importee.cna");
include($cnaScriptPath);

importee.cna

println("Successfully imported importee!");
println(script_resource());
The importer.cna script can be loaded in Cobalt Strike.
Successfully loaded importer.cna
In the Script Console, it displays that the importee.cna script was successfully loaded from the subdirectory. Additionally, it displays the output of script_resource(), indicating that the importee.cna script uses importer.cna's working directory.
importee.cna output after being loaded from importer.cna
This means that BOFs and assets loaded by the importee, use the script_resource() value of the importer. Therefore, without modifications, it would break most existing scripts that load other resources. This is something we would like to avoid during our approach, so let's come up with a better approach.

Decompiling Cobalt Strike

Luckily, Sleep can interact with Java and the Cobalt Strike client itself is written in Java. We will decompile the client to find an alternative, more native way to load agressor scripts, without relying on the default Sleep "include" function. Time to put those OSWE skills to work and analyse the Cobalt Strike client's source code!
First, let's extract the contents of the Cobalt Strike application. On MacOS, this can be done by right-clicking the application > Show Package Contents.
Viewing the Cobalt Strike MacOS app's contents
Next, the files of Contents/Java can b​e copied to a new folder.
Cobalt Strike Contents/Java folder files
We are mainly interested in the cobaltstrike-client.jar, as this is the client-side application. Let's decompile it with JD-GUI. Download the JD-GUI jar from this website and run it.
java -jar jd-gui-1.6.6.jar
Drag the cobaltstrike-client.jar file into the window. You now successfully decompiled the application. Congratulations.

Find The Loader

Now it's a matter of finding the correct class. We can try looking for keywords to find the functionality. For example, when pressing the "Reload" button in the script manager, the UI displays a popup with "Reloaded /path/to/script.cna" in the message.
Reloading importer.cna displays "Reloaded /path/to/script.cna"
if we search for this string in JD-GUi, we only get one result, which is related to the ScriptManager class. Awesome!
The only result is in the ScriptManager.class file
After navigating to the aggressor > windows > ScriptManager.class file, we can observe that before displaying the Dialog, the code simply unloads the selected script and calls the this.client.getScriptEngine().loadScript(str) method to reload it. We already know that str is the path to the aggressor script from the popup output.
this.client.getScriptEngine().loadScript(str) in ScriptManager.class
Interesting. The next thing on the list is to find out what this.client is. It seems to be an AggressorClient object if we double-click it in JD-GUI. ​
this.client is an object of the AggressorClient class

Implement Loader In Sleep

Somewhere in the Cobalt Strike documentation the getAggressorClient() function is already described as a way to obtain an AggressorClient object. Could this be easier than first thought?
$client = getAggressorClient();
Let's adapt the importer.cna script to try to load importee.cna via this method.
# Cobalt Strike-native way of loading a script
$cnaScriptPath = script_resource("subdirectory/importee.cna");
# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#getAggressorClient
$client = getAggressorClient();
[[$client getScriptEngine] loadScript: $cnaScriptPath];
After reloading importer.cna, we achieve partial success. The path of the imported script is now the subdirectory hosting the importee script, which means that we already fixed the context problem!
Successfully loaded the importee.cna script in the context of the subdirectory
However, sometimes it would be great if we could also unload individual scripts that were loaded in this way. This is currently not supported, as can be observed in the script manager UI. The importee.cna script is not visible. Let's #TryHarder.