Cobalt Strike Aggressor Scripts-Ception
Abstract
Over the past few months I've been using Cobalt Strike (CS) quite extensively, both during Simulated Attack engagements and for R&D and offensive security projects. I subsequently used more than what I expected the famous Aggressor script engine.
Throughout the different versions of CS, Raphael Mudge developed multiple features that allow operators to extend the standard capabilities of CS:
- C2 malleable profile, to modify the behaviours of the implant (e.g. SMB name pipe, HTTP/S URI, process spawned for shellcode injection, etc...);
- Aggressor Scripts to modify the CS client (e.g. adding new beacon commands, creating new popups, handling events, etc ...); and
- More recently, beacon object files to execute non-linked C code within a running beacon.
The aim of this small post is not to explain how all these amazing features works, even if this would be super interesting. Instead, I will discuss how I managed to load aggressor scripts from an aggressor script. Hence the scripts-ception title. This may sound completely stupid, but this is something that I was looking for, for a long time. Being able to have a "main" aggressor script that will load all my other scripts.
Cobalt Strike Internal Java Object
First things to know is that aggressor scripts are written with the Sleep scripting languages, which has been developed by Raphale Mudge. One of the interesting feature of Sleep is that it is possible to create, access, and query Java objects through Object expressions, as mentioned here. In other words, this means that it is possible to interact with the internal Java objects, classes, and methods of CS from an aggressor script instead of solely relying on the function exposed by the script engine.
After some basic reverse engineering of the CS JAR file with JD-GUI, the
following class can be identified as the class used for the Script Manger UI
interface: aggressor.windows.ScriptManager
. One interesting
method that is implement is:
actionPerformed(ActionEvent paramActionEvent)
:
public void actionPerformed(ActionEvent paramActionEvent) {
if ("Load".equals(paramActionEvent.getActionCommand())) {
SafeDialogs.openFile("Load a script", null, null, false, false, this);
} else if ("Unload".equals(paramActionEvent.getActionCommand())) {
String str = this.model.getSelectedValue((JTable)this.table) + "";
for (Cortana cortana : Aggressor.getFrame().getScriptEngines())
cortana.unloadScript(str);
List list = Prefs.getPreferences().getList("cortana.scripts");
list.remove(str);
Prefs.getPreferences().setList("cortana.scripts", list);
Prefs.getPreferences().save();
refresh();
} else if ("Reload".equals(paramActionEvent.getActionCommand())) {
String str = this.model.getSelectedValue((JTable)this.table) + "";
try {
this.client.getScriptEngine().unloadScript(str);
this.client.getScriptEngine().loadScript(str);
DialogUtils.showInfo("Reloaded " + str);
} catch (YourCodeSucksException yourCodeSucksException) {
MudgeSanity.logException("Load " + str, (Throwable)yourCodeSucksException, true);
DialogUtils.showError("Could not load " + str + ":\n\n" + yourCodeSucksException.formatErrors());
} catch (Exception exception) {
MudgeSanity.logException("Load " + str, exception, false);
DialogUtils.showError("Could not load " + str + "\n" + exception.getMessage());
}
try {
for (Cortana cortana : Aggressor.getFrame().getOtherScriptEngines(this.client)) {
cortana.unloadScript(str);
cortana.loadScript(str);
}
} catch (Exception exception) {
MudgeSanity.logException("Load " + str, exception, false);
}
refresh();
}
}
As shown and as expected from an object used for UI components, the logic that
actually do the work (i.e. load/unload/reload scripts) is not implemented
here. However, the method calls the getScriptEngine()
method from
the client
property. This property is a protected
aggressor.AggressorClient
object.
The getScriptEngine
method is actually pretty simple because this
is a "getter" to the engine
property of the object. The
property being a protected cortana.Cortana
object.
public Cortana getScriptEngine() {
return this.engine;
}
This is perfect because the Cortana
object is doing what
we want via the following methods:
findScript(String paramString)
,
unloadScript(String paramString)
and
loadScript(String paramString)
. These methods can be used to
respectively, identify whether a script has been already loaded, to unload a
loaded script and to load a new script.
public String findScript(String paramString) {
Iterator<E> iterator = this.scripts.keySet().iterator();
while (iterator.hasNext()) {
String str = iterator.next().toString();
File file = new File(str);
if (paramString.equals(file.getName()))
return str;
}
return null;
}
public void unloadScript(String paramString) {
Loader loader = (Loader)this.scripts.get(paramString);
if (loader == null)
return;
this.scripts.remove(paramString);
loader.unload();
}
public void loadScript(String paramString, InputStream paramInputStream) throws YourCodeSucksException, IOException {
Loader loader = new Loader(this);
if (this.scripts.containsKey(paramString))
throw new RuntimeException(paramString + " is already loaded");
loader.getScriptLoader().addGlobalBridge(this.events.getBridge());
loader.getScriptLoader().addGlobalBridge(this.formats.getBridge());
loader.getScriptLoader().addGlobalBridge(this.myinterface.getBridge());
loader.getScriptLoader().addGlobalBridge(this.utils);
loader.getScriptLoader().addGlobalBridge(this);
loader.getScriptLoader().addGlobalBridge(this.keys);
loader.getScriptLoader().addGlobalBridge(this.menus.getBridge());
for (Loadable loadable : this.bridges)
loader.getScriptLoader().addGlobalBridge(loadable);
if (paramInputStream != null) {
loader.loadScript(paramString, paramInputStream);
} else {
loader.loadScript(paramString);
}
this.scripts.put(paramString, loader);
}
At this point we do not need to go much more in depth because we have all we need. Feel free to dig deeper into the internals of the cortana.Loader
object if you are interested in how CS is loading/unloading the scripts.
Last thing that is interesting is the aggressor.Prefs
object that as the name implies stores the settings/preferences of CS. In our case we can use the getList(String paramString)
andsetList(String paramString, List<?> paramList)
methods to update the scripts that have been loaded/unloaded. These data will be used by the UI.
Load Aggressor Scripts
Based on the information mentioned hereinabove, the following aggressor script can be written:
# Java packages
import aggressor.windows.ScriptManager;
import aggressor.AggressorClient;
import aggressor.Prefs;
import cortana.Cortana;
import java.util.List;
# $1 - array of scripts to load
sub load_aggressor_script {
this('$script $client $cortana $prefs $list');
$script = [new ScriptManager: getAggressorClient()];
$client = [$script client];
$cortana = [$client engine];
# Get preferences
$prefs = [Prefs getPreferences];
$list = [$prefs getList: 'cortana.scripts'];
# Load/Reload scripts
foreach $value ($1) {
println("\c2[+]\c0 Loading: " . $value);
# Unload script if alread loaded
if ([[$cortana scripts] containsKey: $value]) {
[$cortana unloadScript: $value];
[$list remove: $value];
}
# Load script
[$cortana loadScript: $value];
[$list add: $value];
}
# Refresh UI
[$prefs setList: 'cortana.scripts', $list];
[$prefs save];
[$script refresh];
}
# Banner
println('');
println('Cobalt Strike Aggressor Script Utility for Loading Aggressor Scripts');
println('Copyright (C) 2020 Paul Laine (@am0nsec)');
println('https://ntamonsec.blogspot.com/');
println('');
# Scripts to load
@scripts_to_load = @(
"C:\\Users\\redacted\\Desktop\\script1.cna",
"C:\\Users\\redacted\\Desktop\\script2.cna"
);
load_aggressor_script(@scripts_to_load);
The script can be downloaded from the following GitHub Gist: https://gist.github.com/am0nsec/e8c86a249368b46a071d99be44605cc5