At work I’ve been working on a project that will allow us to get an elevated shell on a computer that we own regardless of whether or not the device is on-premises or in the employee’s home. Our EDR solution offers a type of “live response,” but it is severely lacking in capability and usability. What our EDR can do, though, is push a file to an endpoint and execute it.
We could just push any run-of-the-mill reverse shell using our EDR, but we are also working from home, and may someday be in a situation where we need emergency access and are not at the office. Also, many standard reverse shells are unencrypted. So I decided to make my own end-to-end encrypted reverse shell that works from anywhere without having to mess with port forwarding.
How does that work? I decided to take an unconventional approach to the reverse shell client-server model. Basically I’m applying end-to-end encrypted chat application architecture to a reverse shell. In my reverse shell, there are three nodes: a client reverse shell, a client operator/”listener”, and an always on server that merely bridges the two client connections.
Here’s what happens in the flow… First, the reverse shell connects to the server and authenticates itself, and then the server starts listening on the operator port. Then the operator connects to the server and authenticates itself, and the server bridges the two connections. Next, the operator initiates a key exchange with the reverse shell. Once encryption keys are in place, the reverse shell starts the shell and BAM! Anywhere reverse shell.
The idea was straightforward compared to the implementation. I haven’t been challenged by a program this much in quite some time. Maybe since I took Compilers my junior year of college. The challenges with this reverse shell were unexpected, and they started and ended with my lack of understanding of I/O buffers.
My first challenge came when I was first implementing the basic architecture without any encryption. I was using socket file objects on the client sides for the socket I/O, and didn’t realize that writing to a socket file buffer doesn’t automatically send the data. After a while, I discovered I had to flush the file object after writing.
I also ran into some buffering issues with my initial implementation of the server. My first few attempts required a newline character to be sent in order to flush the buffer. In the end, I conjured up some GNU black magic and used “cat” to bridge two sets of socket file descriptors. The code below should help clarify what I mean by that:
fromTarget = targetConnection.makefile("rb") toTarget = targetConnection.makefile("wb") fromOperator = operatorConnection.makefile("rb") toOperator = operatorConnection.makefile("wb") targetToOperator = subprocess.Popen("cat", stdin=fromTarget, stdout=toOperator, stderr=toOperator) operatorToTarget = subprocess.Popen("cat", stdin=fromOperator, stdout=toTarget, stderr=toTarget)
Eventually I got everything working as I had planned, albeit without encryption. Adding the encryption turned out to be the most difficult part of the project.
At first, I thought I would just be able to use Python’s ssl library, wrap the socket in a tls layer, and everything would work just like it did before. I quickly figured out that was a silly assumption. Using TLS with two separate connections and then making them talk to each other created a Tower of Babel scenario where neither end had any way to read each other’s messages.
In an attempt to resolve this, I ended up diving down a pretty deep rabbit hole. I rewrote the server implementation so that it would read and then forward messages rather than just stream them through to each other with cat. Once I got that working, I encountered a weird issue. When the shell launched, it would send the shell header (crap about Microsoft and Windows version), but not the prompt. When I would send a command, it would display the prompt and then the output. Below is an example of what I mean:
Connecting to reverse shell... Microsoft Windows [Version 10.0.19041.685] (c) 2020 Microsoft Corporation. All rights reserved. whoami C:\{path to cwd}>mydomain\cjmay
By troubleshooting, I figured out that for some reason the reverse shell wasn’t even sending the prompt until it received a command. Since I didn’t change anything on either client from when it was working, I figured it had to be a network buffer issue with the server. I tried hard, read all sorts of online forums, and rewrote the server multiple times before understanding that the Python ssl library interacts with the operating system buffers in weird ways and sometimes doesn’t tell the program that there’s data in a buffer. This even breaks things like using ‘select’ in an asynchronous receive process.
Rather than pour more time into trying to figure out if what I was doing was even possible, I decided to rewrite my programs entirely and implement the encryption myself. I went back to my original server implementation and rewrote my clients to perform the key exchange solely between the two clients once they were connected.
This was my first time writing a key exchange for a network program, so it took me some learning to implement this. Once I got it ironed out, I was back to having readable communication between the clients. Immediately my heart sank. I seemed to be having the same error that I had with the ssl library where the prompt was being displayed after I sent a command. But then I noticed one slight difference. The shell seemed to be sending the prompt before it got the command this time!
Connecting to reverse shell... Performing key exchange... Microsoft Windows [Version 10.0.19041.685] (c) 2020 Microsoft Corporation. All rights reserved. whoami C:\{path to cwd}>whoami mydomain\cjmay
I ran some various tests, and it appeared to be true. Not only was the shell sending the prompt before it got the command, but the operator was receiving it! So why wasn’t it printing? After more trying and reading, I finally figured it out. There was ANOTHER buffer that was blocking the output. It was the print statement! Since the prompt didn’t end with a newline character, the print statement was buffering all the previously printed characters on that line until it received a newline.
It turns out, you can turn off a print statement’s buffering by adding flush=True to your print function arguments. After a long process of frustration and growth, I finally have a working product.
Connecting to reverse shell... Performing key exchange... Microsoft Windows [Version 10.0.19041.685] (c) 2020 Microsoft Corporation. All rights reserved. C:\{path to cwd}>whoami mydomain\cjmay
I’m super excited to get this integrated into my existing tooling that I have at work and test it out with my team members. It wouldn’t be a proper troubleshooting post without morals of this story:
- Don’t be afraid to totally change your code when you are really stuck
- Hope you never find yourself in buffer hell