Checklist

  • Identify the server side language and framework.
  • Map every place user input reaches a command, an eval, or a deserializer.
  • List the dangerous functions for that language.
  • Know whether input hits a shell or runs through an exec array.
  • Confirm the target OS so you pick the right metacharacters and shell.

How a request becomes a process

Every RCE has the same shape. Input enters at a source (a parameter, header, cookie, uploaded file, or deserialized blob). It travels to a sink (a function that executes or evaluates). The interpreter runs it. A process or a code path you control starts.

Your job is to find a source that reaches a sink without enough sanitization in between. Everything below is a catalog of sinks per language, plus the shell rules that decide whether your metacharacters survive.

Shell vs no shell

There are two very different cases.

  1. The app passes a single string to a shell (/bin/sh -c "..." on Linux, cmd.exe /c "..." on Windows). Here shell metacharacters work, and command injection is possible.
  2. The app passes an argument array directly to execve with no shell. Here metacharacters are literal data. You cannot inject a second command, but you may still control an argument, which is sometimes enough (argument injection).

Always determine which case you are in. The same function name can do either depending on how it is called.

PHP

PHP ships many command sinks. All of these spawn a shell by default.

  • system($cmd) runs a command and prints output.
  • exec($cmd, $out) runs a command and returns the last line.
  • shell_exec($cmd) or the backtick operator `$cmd` returns full output.
  • passthru($cmd) runs a command and streams raw output.
  • popen($cmd, 'r') and proc_open() open a process with pipes.

Code evaluation sinks:

  • eval($code) runs PHP source.
  • assert($code) evaluates a string as PHP in older versions.
  • preg_replace with the /e modifier in old PHP evaluated the replacement.
  • call_user_func, call_user_func_array, and variable functions $f() when $f is attacker controlled.

Dangerous functions table:

Function Type Shell
system, exec, shell_exec, passthru, popen, proc_open OS command yes
backticks OS command yes
eval, assert PHP code no
preg_replace /e PHP code no
create_function PHP code no
call_user_func, variable function PHP callable no

Minimal PoC: a parameter that lands in system("ping -c 1 $ip") is injectable with 127.0.0.1; id.

Python

Process sinks:

  • os.system(cmd) runs through the shell.
  • subprocess.run(cmd, shell=True) and Popen(cmd, shell=True) run through the shell. With shell=False and a list, no shell is used.
  • os.popen(cmd) runs through the shell.
  • commands.getoutput in Python 2.

Code sinks:

  • eval(expr) evaluates an expression.
  • exec(code) runs statements.
  • pickle.loads(data) runs __reduce__ during unpickling. This is deserialization RCE, covered in its own chapter.
  • yaml.load(data) without SafeLoader can instantiate objects.
Function Type Shell
os.system, os.popen OS command yes
subprocess with shell=True OS command yes
subprocess with list, shell=False OS command no
eval, exec Python code no
pickle.loads object + code no
yaml.load (unsafe) object + code no

Minimal PoC: subprocess.run(f"nslookup {domain}", shell=True) is injectable with x.com; id.

Node.js

Process sinks from child_process:

  • exec(cmd) and execSync(cmd) run through a shell.
  • execFile(file, args) runs a binary with an argument array, no shell by default.
  • spawn(cmd, args, { shell: true }) runs through a shell when shell is true.

Code sinks:

  • eval(code) and the Function(code) constructor.
  • vm.runInNewContext(code) which is not a real sandbox.
  • Template engines and require() of attacker controlled paths.
Function Type Shell
exec, execSync OS command yes
spawn/execFile with shell:true OS command yes
spawn/execFile default OS command no
eval, Function, vm.runIn* JS code no

Minimal PoC: exec("git clone " + repo) is injectable with x; id.

Java

Process sinks:

  • Runtime.getRuntime().exec(String) splits on whitespace and does not use a shell, so simple ; injection fails. exec(String[]) takes an array.
  • ProcessBuilder(command).start() takes a list and does not use a shell.

Because Java does not use a shell by default, OS command injection in Java usually requires the app to call a shell itself, for example exec(new String[]{"bash","-c", userInput}). When you see bash -c or cmd /c with user input, injection works.

Code and object sinks:

  • ObjectInputStream.readObject() deserializes untrusted data and can trigger gadget chains. This is the classic Java RCE, covered in the deserialization sections.
  • Expression languages: SpEL, OGNL, MVEL evaluate strings as code.
  • Scripting engines: ScriptEngineManager running Nashorn or Groovy.
Function Type Shell
Runtime.exec(String[]), ProcessBuilder OS command no
exec with bash -c or cmd /c OS command yes
ObjectInputStream.readObject object + gadget no
SpEL, OGNL, MVEL eval expression code no
ScriptEngine eval script code no

Shell metacharacters

These characters change the meaning of a command line on a POSIX shell.

  • ; runs the next command unconditionally.
  • && runs the next command if the first succeeds. || runs it if the first fails.
  • | pipes output into the next command.
  • & backgrounds the command.
  • `cmd` and $(cmd) run a command and substitute its output (command substitution).
  • > >> < redirect output and input.
  • \n (a raw newline, often sent as %0a) acts like ;.
  • # starts a comment and can cut off trailing text.
  • $IFS is the field separator, useful when spaces are filtered.

If any of these reach a shell unescaped, you can run a second command. Test the cheapest ones first: ;id, |id, $(id), `id`, and a newline.

Linux vs Windows shells

  • Linux default shell is /bin/sh or bash. Separators: ;, &&, ||, |, \n. Substitution: $(...), backticks. Variables: $VAR, ${VAR}.
  • Windows cmd.exe. Separators: &, &&, ||, |. Variables: %VAR%. No $(...). Use & whoami.
  • Windows PowerShell. Separators: ;, |. Subexpression: $(...). Variables: $env:VAR. PowerShell parses very differently from cmd, so know which one runs your input.

On Windows, if you do not know whether cmd or PowerShell handles the string, try both & whoami and ; whoami.

What to take away

Find the sink, learn whether a shell is in the path, then pick the metacharacters that match the OS and shell. The rest of this methodology is applying that idea to each entry point class.