RCE Foundations
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.
- 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. - The app passes an argument array directly to
execvewith 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')andproc_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_replacewith the/emodifier in old PHP evaluated the replacement.call_user_func,call_user_func_array, and variable functions$f()when$fis 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)andPopen(cmd, shell=True)run through the shell. Withshell=Falseand a list, no shell is used.os.popen(cmd)runs through the shell.commands.getoutputin 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)withoutSafeLoadercan 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)andexecSync(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 whenshellis true.
Code sinks:
eval(code)and theFunction(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:
ScriptEngineManagerrunning 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.$IFSis 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/shorbash. 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.