TLDR; Building containers for Red Team applications is easy. I have included a Cobalt Strike Dockerfile for you in the Artifacts section.

So here is my first actual blog post. In this series, I am going to talk about how Red Teams can use the same tools used by developers to both simplify Red Team infrastructure and generally make your life easier. In Part 1, I will be talking about why Red Teams should use containers in their workflows. As our European friends say, “Let’s get stuck in!”

Why Containerize?

Here is a question that I kept asking myself: what the hell is all the rage about containers? Honestly, it is a great question! As a Red Teamer, here are two big reasons why you should care (yes there are likely more, but here are the reasons from my perspective):

Repeatability

Red Teams are in the interesting category of teams since we are often deploying infrastructure into a variety of places. In doing so, we need to fight with various configurations that may be baked into each cloud provider’s OS images. This is not that big of a deal, but when you do run into a stupid configuration that is blocking your software from installing, it can be a huge time sink to try and determine what is going wrong. However, when you package all your code into a container, you know that no matter where you deploy that container to, it will always work the same way.

Ease of Use

Have you ever had someone come up to you and complain that their install of insert-software-name is not working? Or have you ever onboarded a new team member and told them to just go play around with Cobalt Strike just to get familiar with Command and Control (C2) frameworks? I am willing to bet you have answered yes to one of these questions. If you have, then you can benefit from containers. When you build a container, instead of directing that person to the documentation or a Stack Overflow post of all the ways a Cobalt Strike install can fail, you could just containerize the solution and give that person the Dockerfile or a registry to pull from and they are all set.

Key Terms

Great, maybe I have convinced you about containerizing your Red Team tools! Before diving into “how” you do this, I need to define some key terms so I do not lose you in the process:

  • Dockerfile: a text document that contains all the commands a user could call on the command line to assemble an image (From Docker official documentation here)
  • Buildtime Arguments: dynamic arguments passed into Docker at the container “build time” to allow for more control over a container image when building
  • Runtime Arguments: arguments that are passed into the docker run command and then passed as arguments to the container execution point

Building your First Container

Wow, that was boring. Now for the fun stuff!

Let’s work on building your first container. For example’s sake, let’s build a Cobalt Strike container. Before we begin, I will assume you have a very basic understanding of how Docker works. And also, since my team uses Debian-based operating systems, I will be making my example with debian:stable-slim as the base image.

Automating the Cobalt Strike Install

Before we can build a container for Cobalt Strike, we need to automate the commands to install all the dependencies, download the Cobalt Strike package, extract the package, and run the update script which will also license the product. Let’s break each step down…

Install All Dependencies

Since Cobalt Strike is Java-based, we need to make sure that we have Java installed in the container. Additionally, there are other dependencies that we will want to install in the container to make sure everything works as intended. Since Debian uses apt, we can install the latest Java package and other packages using the following command:

apt-get install --no-install-recommends -y ca-certificates curl expect git gnupg iproute2 openjdk-11-jdk wget

Download the Cobalt Strike Package

Great, now that we have all the required dependencies, we can download the package from their website. This should be a simple wget request, right? Well not exactly…

To download Cobalt Strike, go to https://download.cobaltstrike.com/download and enter your license key. You will be redirected to another page where you select your operating system and then download the package. This is a little challenging considering when you enter your license key, you get a token that is part of the download URL. In order to capture that token, you can make the initial GET request with your license key as a query parameter and use some Bash magic to extract that token. In this example, I have set the COBALTSTRIKE_LICENSE environment variable which allows me to dynamically reference it in the initial curl request.

export COBALTSTRIKE_LICENSE="<cobaltstrike_license"
curl -s https://download.cobaltstrike.com/download -d "dlkey=${COBALTSTRIKE_LICENSE}" | grep 'href="/downloads/' | cut -d '/' -f3

With this curl command, we get the token that we need to include in the download URL. However, just getting the token does us no good. We need to save the token and use it in a subsequent request. We can do this by exporting the results of the curl command above to an environment variable by wrapping that command in export TOKEN=$(<curl-command>).

export TOKEN=$(curl -s https://download.cobaltstrike.com/download -d "dlkey=${COBALTSTRIKE_LICENSE}" | grep 'href="/downloads/' | cut -d '/' -f3)

Now that we have the token, all we need to do is make a wget to get the cobaltstrike-dist.tgz file and use Bash expansion to expand that token in the wget request.

wget https://download.cobaltstrike.com/downloads/${TOKEN}/latest46/cobaltstrike-dist.tgz

Extracting and Updating Cobalt Strike

Now that we have the package, we can use tar to extract the package.

tar zxf cobaltstrike-dist.tgz 

However, before we can run the teamserver, we need to run the update script to license the package we just downloaded. Since the update script will expect us to provide the license key via standard input, we can use an echo command to give it the key.

echo ${COBALTSTRIKE_LICENSE} | ./update

Creating the Dockerfile

Great, now we have a working version of Cobalt Strike installed! Since a Dockerfile can take and run Bash commands to setup a container image, we can take everything we just learned and put it into a Dockerfile. To start building our first container, run the following commands:

mkdir cobaltstrike
cd cobaltstrike
touch Dockerfile

Using Buildtime Arguments

Using our newly created directory and file, we now can start building out the Dockerfile. In the example above, I exported the COBALTSTRIKE_LICENSE as an environment variable. But how the hell can I mimic that functionality with Docker so I do not need to hardcode my license key into the Dockerfile? This is where buildtime arguments come in. Using a buildtime argument, we will pass the Cobalt Strike license using the build-arg flag so that when we build the container, we have a fully licensed version of Cobalt Strike. The way we do this is by defining a ARG in the Dockerfile and then pass that ARG at build time.

FROM debian:stable-slim

# Required Arguments
ARG COBALTSTRIKE_LICENSE

Now we can use that same environment variable called COBALTSTRIKE_LICENSE we set earlier to pass in the license key.

docker build -t cobaltstrike:latest --build-arg COBALTSTRIKE_LICENSE=$COBALTSTRIKE_LICENSE .

Converting the Manual Commands

Now we need to convert all the manual commands we ran earlier into Dockerfile commands. Thankfully, we can use the RUN command in our Dockerfile to just copy and paste all the commands we ran earlier.

# Install all dependencies
RUN apt-get update && \
	apt-get install --no-install-recommends -y ca-certificates curl expect git gnupg iproute2 openjdk-11-jdk wget && \
	apt-get clean && \
	rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
	update-java-alternatives -s java-1.11.0-openjdk-amd64

# Install and update Cobalt Strike
RUN echo "COBALTSTRIKE_LICENSE: ${COBALTSTRIKE_LICENSE}" && \
  export TOKEN=$(curl -s https://download.cobaltstrike.com/download -d "dlkey=${COBALTSTRIKE_LICENSE}" | grep 'href="/downloads/' | cut -d '/' -f3) && \
	cd /opt && \
	wget https://download.cobaltstrike.com/downloads/${TOKEN}/latest46/cobaltstrike-dist.tgz  && \
	tar zxf cobaltstrike-dist.tgz && \
	rm /etc/ssl/certs/java/cacerts && \
	update-ca-certificates -f && \
	cd /opt/cobaltstrike && \
	echo ${COBALTSTRIKE_LICENSE} | ./update && \
	mkdir /opt/cobaltstrike/mount

Pro Tip: If you think you have errors in your Dockerfile, run the build command we defined earlier. This will tell you exactly where you have issues.

Exposing Ports

One of the most difficult concepts in Docker is networking. Because a container is an entire operating system inside an operating system, the container has no knowledge of the base operating system’s networking configuration. Nor should it. Because of this, we need to tell the container only what it needs to know. For example, what ports should be exposed to the host operating system.

For Cobalt Strike, the most commonly used ports are:

  • 50050/TCP: teamserver connection
  • 443/TCP: HTTPS C2
  • 80/TCP: HTTP C2
  • 53/UDP: DNS C2

Because a container is a self-contained operating system (pardon my pun), we must use the EXPOSE function in our Dockerfile to tell the container to explicitly allow specific ports to be accessed. Doing this in a Dockerfile is very easy.

EXPOSE 50050 443 80 53/udp

By using the EXPOSE definition above, we are explicitly telling the container to allow networking connections to happen on ports 50050/TCP, 443/TCP, 80/TCP, and 53/UDP.

Setting the Entrypoint

The purpose of our container is to be a standalone implementation of a Cobalt Strike teamserver. Up to this point, we have Cobalt Strike being installed into the container and we have told the container what ports should be accessible, but we still have yet to run the teamserver command to stand up the actual teamserver. In Docker, we can use the ENTRYPOINT function to tell the container what program to start when the container starts. This means that when I run this container, I want to have it automatically run the teamserver command.

ENTRYPOINT ["./teamserver"]

In defining this ENTRYPOINT, the container will now boot and immediately execute that command. However, keep in mind that if the teamserver command exits or errors out, the container will stop. Whenever the ENTRYPOINT command exits, the entire container will stop. This is typically a gotcha with most developers who start tinkering around with containers.

Also, for those of you who have played with containers before, you probably know that there is also the CMD command that also exists. The key difference is that when you use CMD and pass in runtime arguments, you need to explicitly tell the container what base binary you want to run (essentially overwriting that CMD in the Dockerfile). If you want to learn more, here is a great blog post that covers the key differences.

Putting it all Together

Wow! You are still reading this? Kudos to you for being dedicated to wanting to learn! Now that we have everything, we need to actually build the container. Let’s put it all together and build this damn thing! If we put all the commands above together, we get a Dockerfile that looks like this:

FROM debian:stable-slim

# Required Arguments
ARG COBALTSTRIKE_LICENSE

# Install all dependencies
RUN apt-get update && \
	apt-get install --no-install-recommends -y ca-certificates curl expect git gnupg iproute2 openjdk-11-jdk wget && \
	apt-get clean && \
	rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
	update-java-alternatives -s java-1.11.0-openjdk-amd64

# Install and update Cobalt Strike
RUN echo "COBALTSTRIKE_LICENSE: ${COBALTSTRIKE_LICENSE}" && \
  export TOKEN=$(curl -s https://download.cobaltstrike.com/download -d "dlkey=${COBALTSTRIKE_LICENSE}" | grep 'href="/downloads/' | cut -d '/' -f3) && \
	cd /opt && \
	wget https://download.cobaltstrike.com/downloads/${TOKEN}/latest46/cobaltstrike-dist.tgz  && \
	tar zxf cobaltstrike-dist.tgz && \
	rm /etc/ssl/certs/java/cacerts && \
	update-ca-certificates -f && \
	cd /opt/cobaltstrike && \
	echo ${COBALTSTRIKE_LICENSE} | ./update && \
	mkdir /opt/cobaltstrike/mount

# Expose the ports and run it
WORKDIR /opt/cobaltstrike
EXPOSE 50050 443 80 53/udp
ENTRYPOINT ["./teamserver"]

And with that Dockerfile in our cobaltstrike directory, we can now build the container image.

docker build -t cobaltstrike:latest --build-arg COBALTSTRIKE_LICENSE=$COBALTSTRIKE_LICENSE .

Using the Container (without a Malleable C2 Profile)

We have a new fancy schmancy Cobalt Strike container. Now we want to use it. This is where we need to use the docker run command.

docker run -it cobaltstrike 192.168.1.1 password

With the command above, we are essentially running the following command ./teamserver 192.168.1.1 password, but it is all in a container. This means that it is all running in its own operating system. The -it flag starts the container in interactive mode. This allows us to see the standard output of the container. We can use ctrl + c on that process to kill the container.

Remember what I said earlier, the hardest part to Docker is networking. Although we are running a container, other computers have no idea that a container is running and listening on the ports we had EXPOSE’d. To fix this, we need to define port proxies in our docker run command. This will tell the host operating system what ports it should listen to and what ports it should map to the container ports. This can be accomplished using the -p <host_port>:<container_port> syntax.

docker run -it -p "50050:50050/tcp" -p "443:443/tcp" -p "80:80/tcp" -p "53:53/udp" cobaltstrike 192.168.1.1 password

From here, we now have a full Cobalt Strike container listening on all interfaces of your host operating system!

Using the Container (with a Malleable C2 Profile)

All of that was super cool, right? But wait a minute… what the hell do I do if I want to use a Malleable C2 profile with the container? This is where bind mounts come into play. We need to tell the container to mount to the host operating system to allow the container to access files on the host. Keep in mind, doing so may have security implications. Make sure you understand those implications before doing this with all of your containers.

Inside the run command we can create a bind mount using the --mount parameter. Let’s say you already have a Malleable C2 profile created called c2.profile and it’s located in the cobaltstrike directory you created earlier. In order to use that profile in the container, you need to create a bind mound to the cobaltstrike directory in the container and then reference that profile (that now exists in the container’s target directory) in the entrypoint.

docker run -it --mount type=bind,source="$(pwd)",target=/opt/cobaltstrike/mount -p "50050:50050/tcp" -p "443:443/tcp" -p "80:80/tcp" -p "53:53/udp" cobaltstrike 192.168.1.1 password /opt/cobaltstrike/mount/c2.profile

With that, you have everything you now need to build and run your very own Cobalt Strike container.

Demonstration

With everything we have learned, we can now build the container image.

After building the image, we can run the container with the bind mount to use our custom C2 profile.

We see the container has booted properly. Now we can use the host operating system’s IP address to connect to the container.

After we hit connect, we become connected to our containerized version of Cobalt Strike!

What’s Next?

Next week, I will release a blog post on how we can leverage a container registry to take this container image and allow other operators use the prebuilt image. We will use one line of code to pull that container down and run it! Stay tuned!

Artifacts

The Dockerfile:

FROM debian:stable-slim

# Required Arguments
ARG COBALTSTRIKE_LICENSE

# Install all dependencies
RUN apt-get update && \
	apt-get install --no-install-recommends -y ca-certificates curl expect git gnupg iproute2 openjdk-11-jdk wget && \
	apt-get clean && \
	rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
	update-java-alternatives -s java-1.11.0-openjdk-amd64

# Install and update Cobalt Strike
RUN echo "COBALTSTRIKE_LICENSE: ${COBALTSTRIKE_LICENSE}" && \
  export TOKEN=$(curl -s https://download.cobaltstrike.com/download -d "dlkey=${COBALTSTRIKE_LICENSE}" | grep 'href="/downloads/' | cut -d '/' -f3) && \
	cd /opt && \
	wget https://download.cobaltstrike.com/downloads/${TOKEN}/latest46/cobaltstrike-dist.tgz  && \
	tar zxf cobaltstrike-dist.tgz && \
	rm /etc/ssl/certs/java/cacerts && \
	update-ca-certificates -f && \
	cd /opt/cobaltstrike && \
	echo ${COBALTSTRIKE_LICENSE} | ./update && \
	mkdir /opt/cobaltstrike/mount

# Expose the ports and run it
WORKDIR /opt/cobaltstrike
EXPOSE 50050 443 80 53/udp
ENTRYPOINT ["./teamserver"]

The build command:

docker build -t cobaltstrike:latest --build-arg COBALTSTRIKE_LICENSE=$COBALTSTRIKE_LICENSE .

The run command (without Malleable C2):

docker run -it -p "50050:50050/tcp" -p "443:443/tcp" -p "80:80/tcp" -p "53:53/udp" cobaltstrike 192.168.1.1 password

The run command (with Malleable C2):

docker run -it --mount type=bind,source="$(pwd)",target=/opt/cobaltstrike/mount -p "50050:50050/tcp" -p "443:443/tcp" -p "80:80/tcp" -p "53:53/udp" cobaltstrike 192.168.1.1 password /opt/cobaltstrike/mount/c2.profile