Ever needed a notifier when a new beacon checks in? Don’t want to keep checking your Cobalt-Strike server every 5 minutes in the hopes of a new callback? We got you covered! Introducing the notification-service aggressor script available at
https://github.com/NVISOsecurity/blogposts/tree/master/cobalt-strike-notifier

If the above image resonates with you, you’ll know that the point between sending out your phish and receiving your first callback is a very stressful time. All kinds of doom scenarios pop into your head… “Did I test my payload sufficiently?”, “Did my email get blocked somewhere along the chain?”, “Did my target pay attention and reported it as a phish?” You can solve some of these issues by introducing “canaries” in your payloads, for example, an image that phones home when the email is opened, an arbitrary HTTP (or DNS) request to a server you control.
There is however one thing you cannot control, even if you really wanted to: WHEN a user will click on your payload. If you are using Cobalt-Strike out of the box, this will result in you having to check your GUI every x minutes/hours/days to see if a beacon dialed in or not, and by the time you see your beacon connecting, it might already be several minutes or even hours that your beacon is active, this is of course less than ideal.
Thankfully Cobalt-Strike allows us to modify or expand its default behavior through the usage of “Aggressor Scripts”. These scripts are developed in “sleep”. Sleep is a java based scripting language developed and invented by Raphael Mudge (the creator of Cobalt-Strike). There are already a ton of aggressor scripts out there and there even was one that closely resembled our use case developed by FortyNorthSecurity:
https://fortynorthsecurity.com/blog/aggressor-get-text-messages-for-your-incoming-beacons/
Albeit close to what we wanted to implement, it was not ticking all boxes for us. The aforementioned aggressor relied on older python code and was using an email-to-text service only available in the US. This aggressor also didn’t provide an “opt-out” which means you’ll have to kill the aggressor on the team server if you wanted to quit having notifications. So we decided to put on our coding hats and started exploring the world of aggressor coding ourselves.
Tackling two problems for the price of one.
Aggressor scripts are profile specific. This means that they get loaded up when you establish your user session and unloaded when you disconnect from the team server.
This is not a problem for normal operations, but for a notification server this might not be what we want. Luckily Raffi (that’s how Raphael Mudge often refers to himself) thought of this and introduced a binary packaged with Cobalt-Strike called “agscript”. This allows to run Cobalt-Strike in headless mode.
Both approaches have their advantages and disadvantages however:
- Headless mode will need you to hardcode all your values in your aggressor script or figure out a way to parse them from your GUI event log window.
This is a bit cumbersome and reduces flexibility in case you want to turn notifications on or off, (assuming you don’t want to get signal/text/mail spammed every time you lateral move). - “Graphical” mode (for the lack of a better term) will unload if you disconnect from your Cobalt-Strike session. You’ll therefore not receive any notifications if beacons check in while you are disconnected.
We decided we wanted full flexibility so we created both a headless and a graphical aggressor.
Meet our notification GUI
Let’s take a look at our graphical aggressor script first:

As you can see here the power of a graphic aggressor really shines, as you can toggle your notifications on and off and fine grain them however you’d like. Let’s take a look under the hood on how this actually works I’ll explain as we go.. 🙂
First of all we define global variables, these can then be used from any function we want.
Then we set debug level to 57, for more information about debug levels check the Sleep manual.
global ('$emailaddress');
global ('$email2textaddress');
global ('$signalphonenumber');
global ('$receivesignalmessages');
global ('$receivemails');
global ('$receivetexts');
global ('$scriptlocation');
debug(57);
Then we define a callback function. This callback function is called when the set preferences button is clicked and basically takes care of parsing the GUI and setting all global variables to their respective values. This also makes sure they persist when you close and reopen the window. The callback function also takes care of error handling in the GUI. For example if you set signal messaging to on but you are not providing a signal number.
sub callback {
$receivemails = $3["emailchkbox"];
$emailaddress = $3["email"];
$email2textaddress = $3["txt2email"];
$receivetexts = $3["textschkbox"];
$signalphonenumber = $3["signalnumber"];
$receivesignalmessages = $3["signalchkbox"];
$scriptlocation = $3["script_location"];
if(($receivemails eq 'true') && (strlen($emailaddress) == 0))
{
show_message("You won't receive emails because you did not input an email address!");
}
else if(($receivetexts eq 'true') && (strlen($email2textaddress) == 0))
{
show_message("mail to text field is empty, you will not receive text messages");
}
else if (($receivesignalmessages eq 'true') && (strlen($signalphonenumber) == 0))
{
show_message("You won't receive signal messages because you did not input a phone number");
}
else
{
show_message("preferences saved successfully!");
}
if (checkError($error))
{
warn("$error");
}
}
The shownotificationdialog function is responsible of drawing our GUI and setting up some default values:
sub shownotificationdialog{
$dialog = dialog("notification preferences",%(email => $emailaddress, txt2email => $txt2email,signalnumber => $signalphonenumber,script_location => "/home/kali/aggressors/mailer.py", emailchkbox => $receivemails,textschkbox => $receivetexts, signalchkbox => $receivesignalmessages),&callback);
dialog_description($dialog, "Get notified when a new beacon calls home.");
drow_text($dialog,"email","Your email address:");
drow_text($dialog,"txt2email","Email address of the mail-to-text provider:");
drow_text($dialog,"signalnumber","Your signal phone number in internation notation(+countrycode):");
drow_text($dialog,"script_location","The location of the mail script on YOUR LOCAL HOST:");
drow_checkbox($dialog,"emailchkbox","Do you want email notifications?");
drow_checkbox($dialog,"textschkbox","Do you want text messages?");
drow_checkbox($dialog,"signalchkbox","Do you want signal messages?");
dbutton_action($dialog,"set preferences");
dialog_show($dialog);
}
The popup aggressor hooks onto the Cobalt-Strike menu button in the Cobalt-Strike GUI. A list of hooks can be found here. Basically this function triggers the shownotificationdialog function whenever the button is pressed:
popup aggressor {
item "Notification preferences" {shownotificationdialog();}
}
The real “magic” however is in the on beacon_initial callback, this method will parse the hostname and the internal ip address from the beacon and will invoke the python script using Sleeps built-in exec function.
on beacon_initial {
local('$computer');
local('$internal');
$computer = beacon_info($1, "computer");
$internal = beacon_info($1, "internal");
if(($receivemails eq 'true') && (strlen($emailaddress) != 0)){
println("executing python $scriptlocation --ip $internal --computer $computer --receive-emails $emailaddress");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-emails --email-address $emailaddress");
}
if(($receivetexts eq 'true') && (strlen($email2textaddress) != 0))
{
println("executing python $scriptlocation --ip $internal --computer $computer --receive-texts --mail_totext $email2textaddress ");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-texts --mail_totext $email2textaddress");
}
if (($receivesignalmessages eq 'true') && (strlen($signalphonenumber) != 0))
{
println("executing python $scriptlocation --ip $internal --computer $computer --receive-signalmessage --signal-number $signalphonenumber");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-signalmessage --signal-number $signalphonenumber");
}
if (checkError($error))
{
warn("$error");
}
};
An important gotcha! As I already mentioned, your aggressor script is bound to your profile, it is not bound to the team server. As a result, “exec” will execute the command on YOUR machine, NOT THE TEAMSERVER. The notifier script has some dependencies the primary one is obviously python3 as it’s a python script. For signal integrating, it’s relying on signal-cli.

Meet our Notification User!
Our headless-mailer aggressor script shares a lot of similarities to our graphic aggressor script:
global ('$emailaddress');
global ('$email2textaddress');
global ('$signalphonenumber');
global ('$scriptlocation');
global ('$receivemails');
global ('$receivetexts');
global ('$receivesignalmessages');
$emailaddress = "";
$txt2emailaddress ="";
$signalphonenumber ="+countrycode";
$scriptlocation = "/some/dir/notifier.py";
$receivemails = "true";
$receivetexts = "false";
$receivesignalmessages = "true";
on beacon_initial {
local('$computer');
local('$internal');
$computer = beacon_info($1, "computer");
$internal = beacon_info($1, "internal");
if(($receivemails eq 'true') && (strlen($emailaddress) != 0)){
say("new beacon detected! Emailing $emailaddress");
println("executing python $scriptlocation --ip $internal --computer $computer --receive-emails $emailaddress");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-emails --email-address $emailaddress");
}
if(($receivetexts eq 'true') && (strlen($email2textaddress) != 0))
{
say("new beacon detected! sending an email to the email to text service!");
println("executing python $scriptlocation --ip $internal --computer $computer --receive-texts --mail_totext $email2textaddress ");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-texts --mail_totext $email2textaddress");
}
if (($receivesignalmessages eq 'true') && (strlen($signalphonenumber) != 0))
{
say("new beacon detected! sending a signal message to $signalphonenumber");
println("executing python $scriptlocation --ip $internal --computer $computer --receive-signalmessage --signal-number $signalphonenumber");
$handle = exec("python $scriptlocation --ip $internal --computer $computer --receive-signalmessage --signal-number $signalphonenumber");
}
}
As already mentioned, headless means that you’ll need to hardcode your variables instead of having the options in the GUI. Once you filled in the variables, you can launch the agscript for a headless connection to your Cobalt-Strike server:
./agscript 127.0.0.1 50050 notification-service demo /home/jean/Documents/Tools/Agressors/headless-notifier.cna
This can be run from anywhere you want, but your session needs to remain open so I recommend running it directly from your teamserver. The syntax is agscript <host> <port> <username> <password> </path/to/cna>. When done successfully a new user will have entered your server:

Now your notification service is ready for action! When a new beacon spawned the notification service will announce it in the event log window + will take the appropriate action.

Now check your email and/or phone, a new message will be waiting for you!
The real magic lies in the python script!?
Not really though, the python script is fairly trivial:
import argparse
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
#change your smtp login details here.
fromaddr = ""
smtp_password=""
smtp_server =""
smtp_port = 587
#change your signal REGISTRATION number here:
signal_registration_number =""
#leave these blank,will be dynamically filled through the aggressor.
smsaddr = ""
mailaddr = ""
parser = argparse.ArgumentParser(description='beacon info')
parser.add_argument('--computer')
parser.add_argument('--ip')
parser.add_argument('--receive-texts', action="store_true")
parser.add_argument('--receive-emails', action="store_true")
parser.add_argument('--receive-signalmessage', action="store_true")
parser.add_argument('--email-address')
parser.add_argument('--mail_totext')
parser.add_argument('--signal-number')
args = parser.parse_args()
toaddr = []
#take care off email and email2text:
if args.receive_texts and args.mail_totext:
toaddr.append(smsaddr)
if args.receive_emails and args.email_address:
toaddr.append(args.email_address)
#message contents:
hostname = args.computer
internal_ip = args.ip
body = "Check your teamserver! \nHostname - " + str(hostname) + "\nInternal IP - " + str(internal_ip)
#email logic
if toaddr:
print("debug")
msg = MIMEMultipart()
msg['From'] = fromaddr
msg['To'] = ", ".join(toaddr)
msg['Subject'] = "INCOMING BEACON"
msg.attach(MIMEText(body, 'plain'))
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
server.login(fromaddr,smtp_password)
text = msg.as_string()
server.sendmail(fromaddr, toaddr, text)
server.quit()
#signal-cli
if args.signal_number and args.receive_signalmessage:
#take care of signal
print(f"{args.signal_number}")
os.system(f"signal-cli -u {signal_registration_number} send -m " + "\"" + str(body) + "\"" + f" {args.signal_number}")
As you can see it’s nothing more than a simple email script with 1 OS command executor for the signal-cli.
This means that, whether you are execution graphical or headless you’ll need to have python3 installed (and available as your default “python”) and signal-cli installed as well in your global path.
If signal-cli is not in your global path you can adapt the python script to take this into account, it only requires a small change. The same would go for python3 not being your default “python” command.
Conclusions
We hope this “deep” dive into the world of Cobalt-Strike aggressors will open the gates for even more awesome aggressor scripts being developed!
Enjoy your beacon notification services, and good luck, have fun in your next engagements!
The code corresponding to this blog post can be found here:
https://github.com/NVISOsecurity/blogposts/tree/master/cobalt-strike-notifier
About the author

Jean-François Maes is a red teaming and social engineering expert working in the NVISO Cyber Resilience team.
When he is not working, you can probably find Jean-François in the Gym or conducting research.
Apart from his work with NVISO, he is also the creator of redteamer.tips, a website dedicated to help red teamers.
Jean-François is currently also in the process of becoming a SANS instructor for the SANS SEC699: Purple Team Tactics – Adversary Emulation for Breach Prevention & Detection course
He was also ranked #1 on the Belgian leaderboard of Hack The Box (a popular penetration testing platform).
You can find Jean-François on LinkedIn , Twitter , GitHub and on Hack The Box.
One thought on “Tap tap… is this thing on? Creating a notification-service for Cobalt-Strike”