PSC Automation: Using Python to Interact With PortShellCrypter.

PortShellCrypter offers up a scripting socket, and a simple utility (pscsh) that allows executing shell scripts on the remote end.

pscsh basically enables you to write a shell script, and have it be executed remotely, by sending it line by line to the remote shell. Output will be returned to pscsh, as well as being displayed in the shell session.

This is pretty useful for some stuff, but if you want to parse the output client side and make logic happen, it would be much nicer (IMO) to have an expect-like scripting environment, or be able to use a client side interpreter like Python handle the whole thing.

So, its basically a Unix socket that exposes a shell. We can work with this.

On your client side, execute ./pscl -S scripting.socket, then pop a shell on your target and run the remote psc, like so.

% ./pscl -S scripting.socket

PortShellCrypter [pscl] v0.65 (C) 2006-2022 stealth -- github.com/stealth/psc

pscl: set up script socket on scripting.socket

pscl: Waiting for [pscr] session to appear ...
% telnet 192.168.0.150 23000
Trying 192.168.0.150...
Connected to 192.168.0.150.
Escape character is '^]'.


BusyBox v1.10.2 (2019-10-14 12:41:41 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# SHELL=/bin/sh /tmp/pscr-armbin 

PortShellCrypter [pscr] v0.65 (C) 2006-2022 stealth -- github.com/stealth/psc


pscl: Seen STARTTLS sequence, enabling crypto.


BusyBox v1.10.2 (2019-10-14 12:41:41 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

# 

We now have our remote shell, and the scripting socket set up.

Lets take an example, and run the demo script (lightly tweaked for this example) provided using pscsh.

% cat script_/helloworld 
unset HISTFILE
export SHELL=/bin/sh
for i in 1 2 3; do
echo "Hello world $i!"
done
echo "plz encode"|/tmp/pscr-armbin -E /dev/stdin
% ./pscsh -S scripting.socket -f script_/helloworld 
unset HISTFILE
# export SHELL=/bin/sh
# for i in 1 2 3; do
> echo "Hello world $i!"
> done
Hello world 1!
Hello world 2!
Hello world 3!
# echo "plz encode"|/tmp/pscr-armbin -E /dev/stdin

PortShellCrypter [pscr] v0.65 (C) 2006-2022 stealth -- github.com/stealth/psc

# 
# ###everlong###
# %                                                   

The encode function here isn't working great, as this device lacks a /dev/stdin, but you get the idea - we can automatically execute any size shell script, line by line, on the remote end and get output.

Now, what I really wanted was a way to talk to the shell from Python, so I could do some basic automation tasks. Lets make that happen. We will write a simple 'shell client' using the socket and telnetlib libraries.

#!/usr/bin/env python3
import socket
import telnetlib
import sys

def sockshell(socketfile):
    print(f"using {socketfile}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    t.interact()

def main(args):
    if len(args) != 2:
        sys.exit(f"use: {args[0]} socket.socket")
    sockshell(socketfile=args[1])

if __name__ == "__main__":
    main(args=sys.argv)

And lets test it...

darren@Darrens-MacBook-Pro src % python3 sockshell.py 
use: sockshell.py socket.socket
darren@Darrens-MacBook-Pro src % python3 sockshell.py scripting.socket 
using scripting.socket
cat /etc/passwd
cat /etc/passwd
admin:x:0:0:root:/:/bin/sh
# 

To close this - we just hit ^C - if we were to exit using exit, the remote psc will quit instead. Something to be aware of.

So we can send commands over a socket, and get output. Wow, such computer science three.

Lets do something a bit more complex - lets first reimplement 'pscsh', to get more of a feel for things.

Reimplementing pscsh was easier than I thought, an One Beer Problem as opposed to a Ten Coffee Problem.

Basically we read in all the lines of a script and send them one at a time, then send our marker.

We then read back until we hit our marker to get the output.

Done.

#!/usr/bin/env python3
import socket
import telnetlib
import sys

def run_script(socketfile, scriptfile):
    print(f"using {socketfile}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    f = open(scriptfile, "r")
    for line in f.readlines():
        t.write(line.encode('ascii'))
    t.write(b"\n")
    t.write(b"##hacktheplanet##\n")
    output = t.read_until(b"##hacktheplanet##") # this may change depending on your remotes PS1
    return output
    

def main(args):
    if len(args) != 3:
        sys.exit(f"use: {args[0]} socket.socket script.sh")
    output = run_script(socketfile=args[1], scriptfile=args[2])
    print(output.decode('ascii'))

if __name__ == "__main__":
    main(args=sys.argv)

And testing it, its pretty much the exact fucking same as the original pscsh.

% python runscript.py ../src/scripting.socket ../src/script_/helloworld
using ../src/scripting.socket
unset HISTFILE
# export SHELL=/bin/sh
# for i in 1 2 3; do
> echo "Hello world $i!"
> done
Hello world 1!
Hello world 2!
Hello world 3!
# echo "plz encode"|/tmp/pscr-armbin -E /dev/stdin

PortShellCrypter [pscr] v0.65 (C) 2006-2022 stealth -- github.com/stealth/psc

# 
# 
# ##hacktheplanet##
%

It actually makes a lot more sense to show this working as a video, to be perfectly honest. I'm unsure if the embed will display at max resolution, so you may need to click "watch on youtube".

Now that we have basically reimplemented server-side scripting, lets do the next part: adding some client side logic. Lets write a pretty horrible piece of Python that will run commands on the remote side, parse the output, and present it to us.

Given the device I am working with has a busybox with pscan, ifconfig and ping on it, lets write a program that does the following:

  1. Gets all interfaces using ifconfig that have a Bcast address.
  2. Runs ping on each broadcast address for 30 seconds.
  3. Parses out all the unique IP addresses from the ping output.
  4. Port scans each of these hosts, using the first 1024 ports.
  5. Prints for us a nice output of hosts and ports discovered.

If pscan doesn't exist, there are other ways to port scan, and there are other ways to do host discovery, I'm just working with what I have on hand here.

To parse the ifconfig output, instead of writing regexes, lets use a python library - ifconfig-parser. Here is some code that gets us the interfaces.

#!/usr/bin/env python3
import socket
import telnetlib
from ifconfigparser import IfconfigParser
import sys

def mk_sockshell(socketfile):
    print(f"using {socketfile}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    return t

def run_ifconfig(shell):
    command = "ifconfig"
    shell.write(command.encode('ascii') + b"\n")
    output = shell.read_until(b"# ")
    ifdata = IfconfigParser(output.decode('ascii'))
    print(ifdata.list_interfaces())

def main(args):
    if len(args) != 2:
        sys.exit(f"use: {args[0]} socket.socket")
    shell = mk_sockshell(socketfile=args[1])
    run_ifconfig(shell)

if __name__ == "__main__":
    main(args=sys.argv)

We get the following output - a nice, easy to handle 'list' we can iterate through!

% python portscanner.py ../src/scripting.socket
using ../src/scripting.socket
['br0', 'eth0', 'lo']
% 

Our next step will be to see which interfaces have an IP address with a broadcast address. This is pretty simple.

#!/usr/bin/env python3
import socket
import telnetlib
from ifconfigparser import IfconfigParser
import sys

def mk_sockshell(socketfile):
    print(f"using {socketfile}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    return t

def get_broadcast_addrs(shell):
    command = "ifconfig"
    shell.write(command.encode('ascii') + b"\n")
    output = shell.read_until(b"# ")
    ifdata = IfconfigParser(output.decode('ascii'))
    bcast_addrs = []
    for interface in ifdata.list_interfaces():
        iface = ifdata.get_interface(name=interface)
        if hasattr(iface, 'ipv4_bcast') and iface.ipv4_bcast != None:
            print(f"{interface}, {iface.ipv4_addr}, {iface.ipv4_bcast}")
            bcast_addrs.append(iface.ipv4_bcast)
    return bcast_addrs

def main(args):
    if len(args) != 2:
        sys.exit(f"use: {args[0]} socket.socket")
    shell = mk_sockshell(socketfile=args[1])
    bcast_addrs = get_broadcast_addrs(shell)
    if len(bcast_addrs) > 0:
        print(f"got {len(bcast_addrs)} broadcast addresses to ping.")

if __name__ == "__main__":
    main(args=sys.argv)

And we run it, to test.

% python portscanner.py ../src/scripting.socket
using ../src/scripting.socket
br0, 192.168.0.150, 192.168.0.255
got 1 broadcast addresses to ping.

Now, I've realized something. We are going to be reusing some code here, so I'm going to break out the "run a command and get its output" into a function in the next example, where we run ping on every broadcast address for 30 seconds to collect IP's. It should make the code less horrible to read. Or more horrible.

I also borrowed some regex from somewhere to parse out the IP's, and implemented a very, very basic port scanner function (because it is only a couple more lines) using the pscan binary in the busybox.

#!/usr/bin/env python3
import socket
import telnetlib
from ifconfigparser import IfconfigParser
import re
import sys

def mk_sockshell(socketfile):
    print(f"using {socketfile}")
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socketfile)
    t = telnetlib.Telnet()
    t.sock = s
    return t

def run_cmd_remote(shell, command):
    shell.write(command.encode('ascii') + b"\n")
    output = shell.read_until(b"# ") # this may change depending on your remotes PS1
    return output

def get_broadcast_addrs(shell):
    output = run_cmd_remote(shell, command="ifconfig")
    ifdata = IfconfigParser(output.decode('ascii'))
    bcast_addrs = []
    for interface in ifdata.list_interfaces():
        iface = ifdata.get_interface(name=interface)
        if hasattr(iface, 'ipv4_bcast') and iface.ipv4_bcast != None:
            print(f"{interface}, {iface.ipv4_addr}, {iface.ipv4_bcast}")
            bcast_addrs.append(iface.ipv4_bcast)
    return bcast_addrs

def extract_uniq_ips(output):
    hosts = []
    regex = re.findall(r'\b(?:\d{1,3}\.){3}\d{1,3}\b',output.decode('ascii'))
    if regex is not None:
        for match in regex:
            if match not in hosts:
                hosts.append(match)
    return hosts

def do_bcast_ping(shell, bcast_addr):
    command = f"/bin/busybox-armv4tl ping -4 -w 30 {bcast_addr}"
    output = run_cmd_remote(shell, command)
    hosts = extract_uniq_ips(output)
    return hosts

def gather_hosts(shell, bcast_addrs):
    hosts = []
    for bcast_addr in bcast_addrs:
        hosts.extend(do_bcast_ping(shell, bcast_addr))
    return hosts

def portscan(shell, host):
    # just for now, outputs ugly
    command = f"/bin/busybox-armv4tl pscan {host}"
    output = run_cmd_remote(shell, command)
    print(output.decode('ascii'))
    

def main(args):
    if len(args) != 2:
        sys.exit(f"use: {args[0]} socket.socket")
    shell = mk_sockshell(socketfile=args[1])
    bcast_addrs = get_broadcast_addrs(shell)
    if len(bcast_addrs) > 0:
        print(f"got {len(bcast_addrs)} broadcast addresses to ping.")
    else:
        sys.exit("found no broadcast addresses to use, sorry bud.")
    hosts = gather_hosts(shell, bcast_addrs=bcast_addrs)
    print(f"got {len(hosts)} hosts...")
    for host in hosts:
        print(host)
        portscan(shell, host)

if __name__ == "__main__":
    main(args=sys.argv)

And here is the output. Not great yet, I have to have a think about making it nicer. It also has a bug where it portscans broadcast, and I will have to rethink the "ping broadcast" method of collection, as its missing some hosts on the network.

% python portscanner.py ../src/scripting.socket
using ../src/scripting.socket
br0, 192.168.0.150, 192.168.0.255
got 1 broadcast addresses to ping.
got 4 hosts...
192.168.0.255
/bin/busybox-armv4tl pscan 192.168.0.255
Scanning 192.168.0.255 ports 1 to 1024
 Port	Proto	State	Service
pscan: Network is unreachable
# 
192.168.0.54
/bin/busybox-armv4tl pscan 192.168.0.54
Scanning 192.168.0.54 ports 1 to 1024
 Port	Proto	State	Service
   81	tcp	open	unknown
1023 closed, 1 open, 0 timed out (or blocked) ports
# 
192.168.0.126
/bin/busybox-armv4tl pscan 192.168.0.126
Scanning 192.168.0.126 ports 1 to 1024
 Port	Proto	State	Service
1024 closed, 0 open, 0 timed out (or blocked) ports
# 
192.168.0.87
/bin/busybox-armv4tl pscan 192.168.0.87
Scanning 192.168.0.87 ports 1 to 1024
 Port	Proto	State	Service
983 closed, 0 open, 41 timed out (or blocked) ports
# 

I guess the next steps would be adding further host-gathering functions in the gather_hosts section, making the port scanner output nicer, maybe doing the port scanning differently...

The nice thing here is all the scanning is done from the box, using tools on it, as opposed to tunnelling nmap through the SOCKS proxy (which, while doable, can be a shitload slower due to the tunnels overhead).

Hopefully though, this showed you a good example of the power of operator-side scripting, as a contrast to purely target side scripting.

Being able to run more powerful shenanigans and processing on output is going to be super useful in future posts.