Categories
red teaming

Shout-out to the Slack Shellbot

After working on the Slacking off with sqlmap post, I began to look for other ways that Slack's webhooks could be helpful in assessments. I stumbled across this post written by Russel Van Tuyl, where he described how to integrate these webhooks to notify him when a shell is received either in Metasploit or PowerShell Empire. I decided since I've been learning how to work with Covenant, that it'd be fun to modify his work to make a Slack bot that notifies when a new grunt is activated.

As Russel points out in his post, this application can come in handy for phishing campaigns and/or through the usage of malicious USB devices as it is often unknown when an unsuspecting user will click that link or plug in that USB device. These notifications when a grunt is activated can allow teams to better manage their attacks moving forward once that initial access has been established.

Setup

The steps to create an app in Slack with a webhook can be found in my Slacking off with sqlmap post. Once the app is up and working, we can modify the work that Russel has done (blog post & Github repo) to match what's found below.

Note: the code needed for Metasploit was removed, and code used for Empire was modified to reflect usage for Covenant instead

shellbot.conf

[slack]
slackHook = https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
botName = shellbot
channel = shellbot
[ShellBot]
sleepTime = 60
[covenant]
db = /opt/Covenant/Covenant/Data/covenant.db

Point to the covenant.db file in the conf file. Also, replace the slackHook variable to match your app's correct link.

shellbot.py

#! /usr/bin/env python

__author__ = 'Russel Van Tuyl'
__maintainer__ = "Russel Van Tuyl"
__email__ = "Russel.VanTuyl@gmail.com"
__version__ = "1.2.1"

import sqlite3
import datetime
import requests
import time
import configparser
import sys
import os
import argparse
#################################################
#                   COLORS                      #
#################################################
note = "\033[0;0;33m[-]\033[0m"
warn = "\033[0;0;31m[!]\033[0m"
info = "\033[0;0;36m[i]\033[0m"
question = "\033[0;0;37m[?]\033[0m"
debug = "\033[0;0;31m[DEBUG]\033[0m"

ssb_root = os.path.dirname(os.path.realpath(__file__))
runTime = datetime.datetime.now()
sleepTime = 60
slackHook = None
botName = None
channel = None
covenantDb = None
knownAgents = {"covenant": []}

DEBUG = False
VERBOSE = False

def db_query(dbPath):
    """Query sqlite database"""

    agents = {}

    try:
        connection = sqlite3.connect(dbPath)
        rs = connection.execute("SELECT Id, Name, OriginalServerGuid, GUID, Children, ImplantTemplateId, ValidateCert, UseCertPinning, SMBPipeName, ListenerId, Note, Delay, JitterPercent, ConnectAttempts, KillDate, DotNetFrameworkVersion, Status, Integrity, Process, UserDomainName, UserName, IPAddress, Hostname, OperatingSystem, GruntSharedSecretPassword, GruntRSAPublicKey, GruntNegotiatedSessionKey, GruntChallenge, ActivationTime, LastCheckIn, PowerShellImport from Grunts;")

        for r in rs:
            agents[r[0]] = {'Id': r[0],
                                'Name': r[1],
                                'OriginalServerGuid': r[2],
                                'GUID': r[3],
                                'Children': r[4],
                                'ImplantTemplateId': r[5],
                                'ValidateCert': r[6],
                                'UseCertPinning': r[7],
                                'SMBPipeName': r[8],
                                'ListenerId': r[9],
                                'Note': r[10],
                                'Delay': r[11],
                                'JitterPercent': r[12],
                                'ConnectAttempts': r[13],
                                'KillDate': r[14],
                                'DotNetFrameworkVersion': r[15],
                                'Status': r[16],
                                'Integrity': r[17],
                                'Process': r[18],
                                'UserDomainName': r[19],
                                'UserName': r[20],
                                'IPAddress': r[21],
                                'Hostname': r[22],
                                'OperatingSystem': r[23],
                                'GruntSharedSecretPassword': r[24],
                                'GruntRSAPublicKey': r[25],
                                'GruntNegotiatedSessionKey': r[26],
                                'GruntChallenge': r[27],
                                'ActivationTime': r[28],
                                'LastCheckIn': r[29],
                                'PowerShellImport': r[30]
                                }

        connection.close()
    except sqlite3.OperationalError as e:
        print(warn + "Error connecting to the database at %s" % dbPath)
        print(e)

    return agents

def send_new_agent_message_slack(agentType, payload):
    """Send New Agent Message to Slack"""

    if DEBUG:
        print(debug + "New Slack agent message agent: %s, payload: %s" % (agentType, payload))

    text = "[+]New %s agent check in\n%s" % (agentType, payload)
    if agentType == "covenant":
        json_payload = {"channel": channel, "username": "ShellBot", "text": text, "icon_emoji": ":covenant:"}
    else:
        json_payload = {"channel": channel, "username": "ShellBot", "text": text}

    headers = {'content-type': 'application/json'}

    response = requests.post(slackHook, json=json_payload, headers=headers)

    if DEBUG:
        print(debug + "%s" % response.text)
        print(debug + "%d" % response.status_code)
    if response.status_code == 200:
        print("\033[0;0;92m[+]\033[0mNew %s agent check in successfully posted to Slack" % agentType)
        print("\t" + note + "%s" % payload)
    else:
        print(warn + "Message not posted to Slack. HTTP Status Code: %s" % response.status_code)

def parse_config(configFile):
    """Parse the ShellBot configuration file and update global variables"""

    global sleepTime
    global slackHook
    global botName
    global channel
    global covenantDb

    if VERBOSE:
        print(note + "Parsing config file at %s" % configFile)

    c = configparser.ConfigParser()
    c.read(configFile)

    if c.has_section("slack"):
        if c.has_option("slack", "slackHook"):
            slackHook = c.get("slack", "slackHook")
        else:
            print(warn + "Configuration file missing 'slackHook' parameter in 'slack' section")
            sys.exit(1)
        if c.has_option("slack", "botName"):
            botName = c.get("slack", "botName")
        else:
            print(warn + "Configuration file missing 'botName' parameter in 'slack' section")
            sys.exit(1)
        if c.has_option("slack", "channel"):
            channel = c.get("slack", "channel")
        else:
            print(warn + "Configuration file missing 'channel' parameter in 'slack' section")
            sys.exit(1)
    else:
        print(warn + "Missing 'slack' section in configuration file")
        sys.exit(1)

    # This section can be missing, will use global variables instead
    if c.has_section("ShellBot"):
        if c.has_option("ShellBot", "sleepTime"):
            sleepTime = c.getint("ShellBot", "sleepTime")

    if c.has_section("covenant"):
        if c.has_option("covenant", "db"):
            e = c.get("covenant", "db")
            if os.path.isfile(os.path.join(ssb_root, e)):
                covenantDb = os.path.join(ssb_root, e)
            else:
                print(warn + "ShellBot will continue without covenant because database was not found at %s" \
                             % os.path.join(ssb_root, e))
        else:
            print(warn + "ShellBot will continue without covenant because database path not provided.")
    else:
        print(warn + "ShellBot will continue without covenant because configuration was not provided.")

def check_covenant_agents(db):
    """Check for new covenant agents"""

    global knownAgents

    agents = db_query(db)

    if DEBUG:
        print(debug + "%s" % agents)
    if VERBOSE:
        print(info + "Currently checked in agents:")
        for a in agents:
            print("\t" + info + "Session ID: %s\t Checkin Time: %s" % (a, agents[a]['checkin_time']))
    for a in agents:
        try:
            checkin = datetime.datetime.strptime(agents[a]['LastCheckIn'][:-1], "%Y-%m-%d %H:%M:%S.%f")
        except:
            checkin = datetime.datetime.strptime(agents[a]['LastCheckIn'], "%Y-%m-%d %H:%M:%S")
        if a not in knownAgents["covenant"]:
            knownAgents["covenant"].append(a)
            if checkin > runTime:
                if slackHook is not None and slackHook != "" and \
                        slackHook != "https://hooks.slack.com/services/<randomstuff>":
                    msg = "Agent ID: %s\nCheckin Time: %s\nHostname: %s\nIP Address: %s\nUsername: %s\nIntegrity: %s\n" % (agents[a]['Id'], agents[a]['LastCheckIn'], agents[a]['Hostname'], agents[a]['IPAddress'], agents[a]['UserName'], agents[a]['Integrity'])
                    send_new_agent_message_slack("covenant", msg)
                else:
                    if VERBOSE:
                        print(note + "Slack hook not provided, skipping")

if __name__ == '__main__':

    try:
        parser = argparse.ArgumentParser()
        parser.add_argument('--debug', action='store_true', default=False, help="Enable debug output to console")
        parser.add_argument('-v', action='store_true', default=False, help="Enable verbose output to console")
        args = parser.parse_args()
        VERBOSE = args.v
        DEBUG = args.debug

        conf = os.path.join(ssb_root, "shellbot.conf")
        parse_config(conf)

        if (covenantDb is not None):
            print(info + "ShellBot started...")
            while True:
                if covenantDb is not None:
                    check_covenant_agents(covenantDb)
                if VERBOSE:
                    print(info + "Sleeping for %s seconds at %s" % (sleepTime, datetime.datetime.now()))
                time.sleep(sleepTime)
        else:
            print(warn + "Unable to locate or communicate with any C2 servers. Quitting")
            sys.exit(1)

    except KeyboardInterrupt:
        print("\n" + warn + "User Interrupt! Quitting....")
    except SystemExit:
        pass
    except:
        print("\n" + warn + "Please report this error to " + __maintainer__ + " by email at: " + __email__)
        raise

Testing

Once the code is modified, I ran the app using:

python3 shellbot.py

Now that the app is checking for newly created grunts, I started a new grunt, as seen in Covenant's dashboard.

However, if you're away from your system and a grunt is activated, we can now be notified of this through Slack.

This functionality can also be replicated in Cobalt Strike. Jeff Dimmock (@bluescreenofjeff) goes into detail in his post here how to integrate some Aggressor scripting to make use of Slack notifications.

References

  • https://www.swordshield.com/blog/slackshellbot/
  • https://github.com/Ne0nd0g/shellbot
  • https://bluescreenofjeff.com/2017-04-11-slack-bots-for-trolls-and-work/
  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    one + 4 =