Remote code execution (RCE), explained: what it is and how to prevent it

Remote code execution (RCE) is a class of software security flaws/vulnerabilities. RCE vulnerabilities will allow a malicious actor to execute any code of their choice on a remote machine over LAN, WAN, or internet. RCE belongs to the broader class of arbitrary code execution (ACE) vulnerabilities. With the internet becoming ubiquitous, though, RCE vulnerabilities’ impact grows rapidly. So, RCEs are now probably the most important kind of ACE vulnerability.

As that’s the case, we wanted to take a more detailed look at the various types of RCE vulnerabilities and the possible countermeasures.

RCE classification by origin

Most, if not all, of the known RCE vulnerabilities have a small number of underlying causes.

Dynamic code execution

Dynamic code execution tends to be the most common attack vector leading to RCE. Most programming languages have some way to generate code with code and execute it on the spot. This is a very powerful concept that helps solve many complex problems. However, a malicious third party can easily abuse it to gain RCE capabilities.

Often, the code generated at runtime is based on some user input. More often than not, the code includes that input in some form. A malicious actor, realizing that the dynamic code generation will make use of a given input, could provide valid code as an input to attack your application. If the user inputs are not vetted, then that code will be executed on the target machine.

Broadly speaking, dynamic code execution causes two major classes of RCE vulnerabilities: direct and indirect.

Direct

In the case of direct dynamic code execution, the malicious actor is aware that their input would be used in code generation.

Indirect

An indirect case, again, boils down to dynamic code generation including user inputs. The user input, however, passes through one or more layers. Some of the layers might even transform that input before it ends up with dynamic code generation. Also, dynamic code generation might be a side effect and not the primary usage of the input. As such, it’s not really evident to the user providing the input that the input will be used as a building block in a code snippet to be executed on a remote machine.

Deserialization

Deserialization is a very good example of this scenario. Seemingly no dynamic code generation should happen on deserialization. That is actually the case when the serialized object contains only data fields of primitive types or other objects of that kind. Things get more complicated, however, when methods/functions of an object are serialized. Deserialization then will usually include some form of dynamic code generation.

You might think that dynamic languages are the only place where function serialization makes sense. The problem will be of limited scope then. But it’s a useful scenario in static languages, too. It’s somewhat harder to achieve in a static language but by far not impossible.

Quite often, the implementation consists of deserialization-generated proxy objects/functions. Generating objects/functions at runtime is a case of dynamic code generation. So, if the data to be deserialized originates from a request made by a remote machine, a malicious actor could modify it. Carefully crafted serialized code snippets can be injected that trick the dynamic code generation to execute them when invoked as part of the deserialization.

Memory safety

Another cause of RCE vulnerabilities has to do with memory safety. Memory safety means preventing code from accessing parts of memory that it did not initialize or get as an input. Intuitively, you might expect a lack of memory safety to result in unauthorized data access. However, the operating system and the underlying hardware use memory to store actual executable code. Metadata about code execution is also stored in memory. Getting access to this kind of memory could result in ACE and possibly RCE. So what are the main reasons behind memory safety issues?

Software design flaws

Software design flaws are a type of memory safety vulnerability where there’s a design error in some underlying component. More often than not, that would be a compiler, interpreter, or virtual machine, or potentially the operating system kernel or libraries. There are a number of different flaws that belong to this class. We’re going to take a more detailed look at what’s arguably the most common one.

Buffer overflow or buffer overread

Buffer overflow (also known as buffer overread) is a fairly simple and well-known technique to violate memory safety. It exploits a design flaw or a bug to write to the memory cells that follow the actual end of a memory buffer. The buffer itself gets returned from a legitimate call to public API. However, the buffer only serves as a point of origin to compute the physical memory addresses of private field/member values of some object or program counter. Their relative position to the buffer is either well known or might be guessed. Researching the code if available or debugging the program execution at runtime might help a malicious actor obtain relative positions.

So, a buffer overflow allows for modifying memory that should be inaccessible by design. That buffer might reside in the address space of another machine and be modified by calling a remote API. That will allow access to the remote machine memory. Clearly there are various ways to use this type of access in instrumenting an RCE. The general assumption is that if a buffer overflow vulnerability exists, then an RCE is possible. So, code owners should fix buffer overflows ASAP, well before the actual RCE attack emerges.

Scope

More often than not, buffer overflow targets C/C++ code since these languages do not have built in buffer size checks. A lot of other popular frameworks and technologies end up using C/C++ libraries deep down under the surface that automatically makes them vulnerable to this kind of attack.

Node.js is a good example of this since, besides being based on C/C++, JavaScript runtime also allows for native C/C++ add-ons. Because of this, an attacker can carefully craft the requests to a Node.js server to cause buffer overflow and thus modify the system memory on the affected machine, causing execution of arbitrary code.

Hardware design flaws

Interestingly enough, memory safety violations can occur because of hardware security design flaws as well. While less common and harder to find, such vulnerabilities usually have an extremely high impact.

Deflecting RCE attacks

While the outcome of every RCE attack is the same in terms of an attacker executing code, the attack vectors are very different in nature. Blocking all of them takes significant effort. Moreover, the effort grows together with the technology stack. All the attack vectors described in this post are technology-agnostic. All implementations are technology-specific, though, and so are the defense mechanisms.

So, a traditional time-saving approach is to monitor network traffic for suspicious content instead of monitoring each endpoint with its specific technology. A web application firewall (WAF) typically performs this job. While that saves time, it also comes at a price—the WAF is a network performance bottleneck, and it lacks all the background information available at the actual endpoint or at the application and user levels. Hence, WAF traffic analysis could never be perfect. Heuristics is inevitable without full data, so either not all threats will emerge or false positives will arise, or most often both.

Moving inside the app: Sqreen’s approach

Sqreen addresses these WAF deficiencies without increasing the development cost for the end user by moving visibility inside the application, bringing more complete protection with a technology-specific RASP and In-App WAF. Sqreen’s RASP and WAF runs inside the actual web application, API, or microservice receiving network traffic. It doesn’t require any code modification, though. It uses instrumentation points specific for each technology (e.g., JVM API for Java, v8 API for Node.js, etc.) to modify code before execution at runtime. Thus, it’s capable of monitoring and modifying system and network events while having the full context of everything that happens inside the application.

Thus, Sqreen can detect that the app is using components with known memory safety issues. It can also detect the actual user inputs that make it to the dynamic code execution events. Naturally, this is a superior approach to detecting and preventing RCEs as compared to a traditional WAF that has access to network traffic only.

Wrapping up

Clearly, RCE is a very potent attack vector. But, luckily, it’s possible to defend yourself against RCE attacks, too. The information above can really aid in building your defense strategy. If you’re interested in other attack vectors and details, check out our previous posts on SQL injection, XXE, and LFI

—-

This post was written by Tsviatko Yovtchev. Tsviatko  is the original creator and longtime development lead of a number of tools currently used by millions of developers on a daily basis—JustDecompile (the fastest .NET decompiler), Fiddler Everywhere, NativeScript, JustAssembly. He is also a seasoned manager who has created dozens of teams from scratch.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like