04 Mar 2018

CCDC Red Team PWNboard

I built an operations tool for our Midwest CCDC Red Team called PWNboard. Our biggest challenge in these competitions is that we’re in a virtual environment where teams can revert the machines at any point and essentially kill our access. So monitoring the checkins and backdoors is essential to maintaining access for the duration of the event. And that’s why I made PWNboard, an operational board that monitors our implants and backdoors. As Tim MalcomVetter put it, it’s offensive inventory management.



We’ve tried to tackle this problem in the past by using a single central Cobalt Strike team server and other similar approaches. But as any good Red Teamer knows, limiting yourself to a single platform isn’t going to work in a diverse environment.

The main principles that I had for building it was that it had to easily integrate into existing tools (Empire, Metasploit, Cobalt Strike, etc.) and needed to be decentralized. Since our Red Team already used Slack as a communication platform, it was a no-brainer to use Slack as the hub for the implant communication. Below is an overview of the data flows that make PWNboard work.

PWNboard sequence diagram

Slack Integration

I’ve never built any tools around Slack before, but I found a custom “App” with an Incoming Webhook seemed to be the easiest integration. I really just read the documentation and tried things until I got the setup I was looking for.

shellz Slack App

As far as configuring the Slack app, there’s two main pieces to the setup. You need set up the incoming webhooks. This will be the URL embedded in each tool for the status updates. IMHO, webhooks are a better approach as they can only post messages to specific channels and limits the impact of compromised URL as compared to legacy Slack tokens.

shellz Incoming Webhooks

The second feature to configure is the Event Subscriptions. You’ll need to set up an event handler URL, this is my PWNboard IP, and subscribe to message.channels. The subscription will send any message sent to a channel to your event subscription for parsing. Unfortunately, I couldn’t figure out how to only send messages from a specific channel, so the PWNboard looks for a message sent to the shellz channel in its code.

shellz Event Subscriptions


The PWNboard is written in python using Flask and utilizes Redis as a data store. It’s got two routes, /slack-events to handle incoming messages from Slack and / the board itself.

The /slack-events also handles a challenge required by the slack Events API:

@app.route('/slack-events', methods=['POST'])
def slack_events():
    res = request.json
    if res.get('challenge', None):
        return request.json['challenge']

Implant Telemetry

We primarily used Empire in our Midwest Quals for the Windows boxen, but utilized custom backdoors for the Linux systems. I’ll cover how I integrated both to show how easy it was to add telemetry to existing and custom tools.


To make Empire work with the PWNboard, I needed to make three modifications: change the existing Slack help function to utilize the webhook of the shellz Slack app, modify the Slack message so it contained all the required data in a parsable format and add a new Slack call when an agent checks in. The mods I made were quick and dirty. I’d want to refactor them before I ever thought about making a pull request. All of the diffs below are based off the 5d196c409bef8e180727824342b917523e9bb8e3 commit.

The slackMessage helper modification, was modified to utilize the shellz webhook:

git diff lib/common/helpers.py
diff --git a/lib/common/helpers.py b/lib/common/helpers.py
index 2322a97..0298056 100644
--- a/lib/common/helpers.py
+++ b/lib/common/helpers.py
@@ -908,7 +908,7 @@ class KThread(threading.Thread):
         self.killed = True

 def slackMessage(slackToken, slackChannel, slackText):
-       url = "https://slack.com/api/chat.postMessage"
-       data = urllib.urlencode({'token': slackToken, 'channel':slackChannel, 'text':slackText})
-       req = urllib2.Request(url, data)
-       resp = urllib2.urlopen(req)
+    import requests
+    data = {'username': 'shellz', 'channel':slackChannel, 'text':slackText}
+    res = requests.post("https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX", json=data)

The last two mods were straight forward to implement in lib/common/agents.py.

git diff lib/common/agents.py
diff --git a/lib/common/agents.py b/lib/common/agents.py
index a0d1a76..8998dcd 100644
--- a/lib/common/agents.py
+++ b/lib/common/agents.py
@@ -56,6 +56,7 @@ Most methods utilize self.lock to deal with the concurreny issue of kicking off

 # -*- encoding: utf-8 -*-
+import socket
 import os
 import json
 import string
@@ -868,6 +869,10 @@ class Agents:
         Update the agent's last seen timestamp in the database.

+        h = socket.gethostname()
+        slackText = "%s empire agent %s checked in" % (h, sessionID)
+        helpers.slackMessage("","shellz",slackText)
         current_time = helpers.get_datetime()
         conn = self.get_db_connection()
@@ -1325,9 +1330,9 @@ class Agents:

             slackToken = listenerOptions['SlackToken']['Value']
             slackChannel = listenerOptions['SlackChannel']['Value']
-            if slackToken != "":
-                slackText = ":biohazard: NEW AGENT :biohazard:\r\n```Machine Name: %s\r\nInternal IP: %s\r\nExternal IP: %s\r\nUser: %s\r\nOS Version: %s\r\nAgent ID: %s```" % (hostname,internal_ip,external_ip,username,os_details,sessionID)
-                helpers.slackMessage(slackToken,slackChannel,slackText)
+            h = socket.gethostname()
+            slackText = "%s new agent on %s; agent: %s; platform: %s; type: empire" % (h, external_ip, sessionID, os_details)
+            helpers.slackMessage(slackToken,slackChannel,slackText)

                # signal everyone that this agent is now active
             dispatcher.send("[+] Initial agent %s from %s now active (Slack)" % (sessionID, clientIP), sender='Agents')

Custom Tools

To demo a trivial example, let’s assume we backdoored Linux systems with a user named foo. The goal would be to detect if the backdoor still existed and we might use a script like the one below to simply SSH into the system and post to Slack if that login was successful.

#!/usr/bin/env python

import requests
import paramiko
import socket
import sys

HOSTS = (11,23,39,100)
TEAMS = range(21, 33)
NETWORK = "172.25"
KEY = paramiko.RSAKey.from_private_key_file("/root/ccdc.priv")

def slackMessage(text):
    data = {'username': 'shellz', 'channel':'shellz', 'text':text}
    res = requests.post("https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX", json=data)

def connect(host, username, password=None):
    h = socket.gethostname()
    client = paramiko.SSHClient()

        if password:
        slackText = "%s %s backdoor active on %s"  % (h, username, host)
        print slackText
    except Exception as e:
        print e
        return False

def main():
    for team in TEAMS:
      print "Team %s" % team
      ip = "%s.%s.%s" % (NETWORK, team, 11)
      print ip
      connect(ip, "foo")

if __name__ == '__main__':


The setup might seem a bit kludgy, but in the end it’s really simple to integrate this telemetry into any tools you use as long as they can make a POST request. We used this for the Midwest State Quals definitely had some on-the-fly debugging and development happening. But the PWNboard gave us valuable operations intel up to the end of the competition which was awesome.

Any thoughts, suggestions or feedback, hit me up on twitter: @ztgrace.


Tags: CCDC  redteam  empire  Slack  redis  python 
comments powered by Disqus