`Expect` Is Pretty Cool
Recently I’ve been working through the fantastic UDRL course1 offered by ZeroPoint Security. During the first chapter of the course, the author, Alex Reid, walks through the setup process, part of which includes automating Beacon generation using a combination of screen and the headless Agressor script client, agscript. This works pretty well, apart from in my circumstances.
Well then what’s my circumstances?
Since the New Year, I made a resolution with my ISP (or should I say my ISP made one with me…) to not have any internet until towards the end of the month. As such, I’ve been working on hotspots (and tears…4G doesn’t hit like it used to). This raised some issues when using the proposed solution - my connection was so unstable that the TeamServer would report that listeners didn’t exist. This resulted in the build failing and me having to re-run the build. This doesn’t sound too bad, the build time isn’t long - it’s a short make debug and that’s it running - the issue is that the TeamServer loves to report that the listeners don’t exist, so I find myself rerunning builds a lot.
So here I am naively thinking for a screen-noob like me “hey, why not just have screen detect that the build has failed and retry?” to which I promptly gave myself the reply “No”. Instead, I turned to a combination of both spending too many hours automating something when it would have (maybe) been quicker bearing with the original issue and expect.
What is expect?
I was going to try explain it myself, but to be honest the page for expect2 does it really well IMO:
Expect is a program that “talks” to other interactive programs according to a script. Following the script, Expect knows what can be expected from a program and what the correct response should be. An interpreted language provides branching and high-level control structures to direct the dialogue. In addition, the user can take control and interact directly when desired, afterward returning control to the script.
In practice, this means that expect can identify when a given prompt is displayed and enter a defined value. For example, expect could be instructed to look for the prompt Enter your name: and once identified, it will enter Jack. How is this useful for automating agscript inputs? Well…
Automating agscript
agscript is a headless CobaltStrike client and in this case is used to connect to the TeamServer, load a CNA script and then execute a command to generate some payloads. Oh and all of this is done as part of a Makefile. The three key parts of this take the following commands:
- Connect to TeamServer:
$ ./agscript IP PORT nearly_headless_nick password - Load CNA:
agscript> load "/home/jack/github/parmesan/udrl/udrl_sleepmask_dev.cna" - Generate Payloads:
agscript> genpayload
I’d like to execute steps 1 and 2, then execute step 3 until I well…don’t get an error. The error I keep getting is easy to match with a regex of The listener '.*' does not exist, so all I need to instruct expect to do is:
- Execute the first command - this is run from my development host, so there’s no need for
expectto do any special waiting - Instruct
expectto wait until it sees theagscript>prompt with theagressor.*>regex. When found, load the CNA. - Wait again until the same prompt is identified, then build the payloads.
- If the output contains an error, sleep for a few seconds then go again until either my patience runs dry (10 attempts) or it succeeds.
- Profit?
What does this actually look like? Well, see the below (there’s even some nice comments):
bash code snippet start
#!/usr/bin/expect -f
## Configuration
set timeout 60
set max_retries 10
## Command to execute to connect to TeamServer
set agscript_cmd {
cd /home/jack/cobaltstrike/client/ &&
./agscript IP PORT nearly_headless_nick PASSWORD
}
## Path to the CNA file
set cna_path "/home/jack/github/parmesan/udrl/udrl_sleepmask_dev.cna"
## End Configuration
## Connect to TeamServer using the earlier defined command
spawn bash -c $agscript_cmd
## Wait for aggressor> prompt after connecting to TeamServer
expect {
-re {aggressor.*>} {
puts "Connected to agscript"
}
-re {\[\*\] Windows error codes loaded} {
exp_continue
}
eof {
puts "ERROR: agscript exited unexpectedly"
exit 1
}
timeout {
puts "ERROR: Timed out waiting for agscript prompt"
exit 1
}
}
## Load the CNA file
send "load $cna_path\r"
## Wait until either `already loaded` is returned, if it's been loaded previously
expect {
-re {already loaded} {
puts "CNA already loaded"
}
## Or wait until the agressor> prompt is returned
-re {aggressor.*>} {
puts "CNA loaded"
}
timeout {
puts "ERROR: CNA load timed out"
exit 1
}
}
## And this is a handy little loop that will repeat the genpayload command
set attempt 1
while {$attempt <= $max_retries} {
puts "Running genpayload (attempt $attempt)"
send "genpayload\r"
set listener_error 0
expect {
## Did the command return the error about the listener not existing?
-re {The listener '.*' does not exist} {
set listener_error 1
exp_continue
}
-re {\[\*\] All generated!} {
if {$listener_error} {
puts "All generated seen, but listener errors occurred — retrying"
incr attempt
sleep 7
} else {
puts "genpayload succeeded cleanly"
exit 0
}
}
timeout {
puts "ERROR: genpayload timed out"
exit 1
}
}
}
puts "ERROR: genpayload failed after $max_retries attempts"
exit 1bash code snippet end
So all of that can be saved in an .exceptfile, marked executable with chmod +x and hey presto it can be executed and hopefully work! To add to my use case, this was included in a Makefile with @ ./genpayload.expect and now I can relax and hope it successfully builds in 10 attempts (if not less)!