If you've spent any time configuring user authentication on... Full Story
By Manny Fernandez
October 22, 2026
Anatomy of a Remote Code Execution Attack
From the first probe to post-exploitation, stage by stage, with what defenders see (and miss) at each step.
Executive Summary
Objective: Break a remote code execution (RCE) attack into its component stages so you can recognize it, detect it, and cut it off at the stage where your controls are strongest.
Target audience: Network and security engineers, SOC analysts, and anyone who owns an internet-facing service and wants to understand what “unauthenticated RCE, CVSS 9.8” actually means for their environment.
The short version: RCE is not a single event. It is a chain: discovery, fingerprinting, delivery, trigger, callback, and post-exploitation. The exploit itself lasts milliseconds. Everything around it takes minutes to months, and that is where you win. This post is a defender’s map of that chain. It does not contain working exploit payloads, because you do not need them to defend, and the detection and hardening steps are what actually move the needle.
What RCE Actually Means
Remote code execution is any flaw that lets an attacker run code of their choosing on a target system, over a network, without legitimate access to run code there. It sits at the top of the vulnerability food chain because every other goal (data theft, ransomware, persistence, lateral movement) becomes a follow-on task once an attacker can execute commands.
Three qualifiers decide how bad a given RCE is:
| Qualifier | Worst case | Why it matters |
|---|---|---|
| Authentication | Pre-auth (unauthenticated) | Anyone who can reach the port can exploit it |
| Reachability | Internet-facing service | Mass scanning finds you within hours of disclosure |
| Execution context | root / SYSTEM / appliance kernel | No privilege escalation step needed |
An unauthenticated RCE on an internet-facing edge device that runs as root checks all three boxes. That is why VPN gateways, firewalls, file transfer servers, and load balancers dominate the CISA Known Exploited Vulnerabilities (KEV) catalog.
The Vulnerability Classes Behind RCE
RCE is an outcome, not a bug type. Several distinct bug classes end in the same place. Knowing the class tells you which control stops it:
| Class | Root cause | Primary defense |
|---|---|---|
| OS command injection | User input concatenated into a shell command | Parameterized calls, no shell, strict input validation |
| Insecure deserialization | Untrusted data rebuilt into live objects | Do not deserialize untrusted input; allow-list types |
| Expression / template injection | Input evaluated by an expression or template engine | Sandboxed engines, no user input in templates |
| Memory corruption | Writing outside a buffer, use-after-free | Vendor patching, memory-safe languages, exploit mitigations |
| Unrestricted file upload | Server stores and executes an uploaded file | Validate type, store outside web root, no execute permission |
| SQLi chained to RCE | DB features that execute commands or write files | Parameterized queries, least-privilege DB accounts |
The one pattern under all of them
Nearly every RCE reduces to a single idea: untrusted data is treated as instructions. The classic teaching example is OS command injection, where a diagnostic feature builds a shell string out of a user-supplied field:
# VULNERABLE: user input is concatenated into a shell string
import subprocess
def ping_host(host):
return subprocess.run(f"ping -c 1 {host}", shell=True,
capture_output=True, text=True).stdout
The flaw is that the shell, not the application, decides where the “host” ends. The fix is not a smarter blocklist. It is removing the shell from the path and validating the input as the thing it is supposed to be:
# FIXED: no shell, argument vector, strict type validation
import ipaddress, subprocess
def ping_host(host):
ip = ipaddress.ip_address(host) # raises on anything that is not an IP
return subprocess.run(["ping", "-c", "1", str(ip)],
capture_output=True, text=True).stdout
Two changes carry the weight: shell=False (the default when you pass a list) means no shell metacharacters are interpreted, and ipaddress.ip_address() rejects anything that is not a valid address before it ever reaches ping. The same principle, enforce structure at the trust boundary, defeats deserialization, template, and SQL injection flaws too.
The Attack Chain, Stage by Stage

The lab addressing below follows the InfoSecMonkey convention: 198.18.0.0/15 stands in for public IPs and 10.0.0.0/16 for internal networks. The victim is a web application 10.0.20.10 in the DMZ, published through a VIP on 198.18.10.10.
Stage 1: Discovery
Attacker goal: Find every host running the vulnerable software.
Modern RCE campaigns rarely start with a chosen victim. They start with a vulnerable version and a list of every IP running it. Internet-wide scanners and search engines (Shodan, Censys, FOFA) index banners, TLS certificates, favicon hashes, and HTTP titles. Within hours of a public advisory, someone has a target list.
What you see: Almost nothing. Scanning traffic looks like the background radiation every public IP absorbs all day.
Where to act: Attack surface reduction. If the management interface or SSL VPN portal is not exposed, it is not on the list. Know your external footprint better than the scanners do. Run your own external discovery regularly so your inventory matches reality.
Stage 2: Fingerprinting
Attacker goal: Confirm the exact product and version before spending an exploit.
The attacker requests version-revealing content (static assets with build hashes, login page resources, API version endpoints) or sends a harmless probe that behaves differently on patched versus unpatched builds. This stage separates “running the product” from “running a vulnerable build of the product.”
What you see: Requests for unusual static paths, often from cloud-hosted or recently-registered IPs, often with scripted or default User-Agent strings. IPS signatures for many RCE CVEs fire here first, on the recon and version check, before any exploit is attempted.
Where to act: IPS with the scanner and reconnaissance signature categories enabled, reputation-based blocking of known scanning infrastructure, and suppressing version banners wherever the product allows it.
Stage 3: Delivery
Attacker goal: Get the malicious input to the vulnerable code path.
This is where the crafted input reaches the vulnerable parameter: a specific header, form field, URI, cookie, or uploaded file, depending on where the bug lives. The important point for defenders is structural, not the payload contents: the request is almost always malformed or anomalous in a way a proxy can see. It is unusually large, it carries encoding that the legitimate client never uses, it targets a parameter with data of the wrong type, or it hits an endpoint no real user hits.
What you see: With deep inspection in place, the anomaly is visible even when the specific exploit is novel. Oversized headers, nested or double encoding, binary content in text fields, and content-length or protocol violations are all detectable without a signature for that exact CVE.
Where to act: This is the strongest choke point for most organizations. A WAF or inline proxy that normalizes and validates requests, enforces size and encoding limits, and runs IPS signatures catches a large share of delivery attempts here, including the commodity re-use of a public exploit that follows every major disclosure.
Stage 4: Trigger
Attacker goal: Make the vulnerable code execute the injected logic.
Delivery and trigger are often the same request, but conceptually they differ: delivery places the input, trigger is the moment the flawed parser, deserializer, or interpreter acts on it. This is the millisecond where “data” becomes “code.” It happens inside the application, so network controls cannot see it directly. What they can see is the before (anomalous delivery) and the after (unexpected behavior).
What you see: Nothing on the wire at the instant of execution. The tell is on the host: a web server process spawning a shell or interpreter, a service writing to a path it never writes to, or an appliance daemon crashing and restarting (a common side effect of memory-corruption exploits that do not land cleanly).
Where to act: Endpoint detection and response (EDR) and host logging. A web server whose child process is a command shell is one of the highest-fidelity RCE signals that exists.
Stage 5: Callback
Attacker goal: Turn code execution into interactive control.
Code execution alone is blind. To do anything useful, the attacker typically has the compromised host reach back out to infrastructure they control, establishing a channel for commands and data. This outbound connection is the single most reliable detection opportunity in the whole chain, because it violates a simple expectation: your DMZ web server has no business initiating connections to the internet.
What you see: A server in a server VLAN or DMZ making outbound connections, often to a raw IP rather than a name, often on an odd port, sometimes beaconing at a regular interval, sometimes tunneled inside DNS or HTTPS to look normal.
Where to act: Egress filtering is the highest-value, most-neglected control here. Default-deny outbound from server segments, allowing only the destinations and ports a server legitimately needs, turns a successful exploit into a dead end. Outbound IPS, DNS filtering, and botnet/C2 reputation feeds catch what slips past the policy.
Stage 6: Post-Exploitation
Attacker goal: Accomplish the actual objective: persistence, credential theft, lateral movement, data exfiltration, or ransomware staging.
Once there is a reliable channel, the attacker’s work starts. They establish persistence so a reboot or patch does not evict them, harvest credentials to move laterally, map internal systems, and stage data for exfiltration. For most intrusions this phase is measured in days to months, not seconds. It is also the phase with the most observable activity: internal scanning, authentication anomalies, new scheduled tasks or services, and large or unusual outbound transfers.
What you see: East-west scanning and connection attempts, credential use that does not match normal patterns, and data movement toward the egress edge.
Where to act: Internal segmentation to blunt lateral movement, strong authentication to limit credential re-use, DLP and egress controls on exfiltration, and SIEM correlation to connect the dots across host, network, and identity logs.
Where the Chain Breaks: Mapping Controls to Stages
No single control stops RCE. Defense in depth means each stage meets a different control, so one failure does not become a breach:
| Stage | Primary control | Backstop |
|---|---|---|
| Discovery | Attack surface reduction | External posture monitoring |
| Fingerprinting | IPS recon signatures, banner suppression | IP reputation / scanner blocking |
| Delivery | WAF / proxy normalization, IPS | Virtual patching |
| Trigger | Patching (removes the bug) | EDR behavioral detection |
| Callback | Egress filtering (default-deny) | Outbound IPS, DNS/C2 filtering |
| Post-exploitation | Segmentation, strong auth | SIEM correlation, DLP |
FortiGate: Practical Controls by Stage
Everything above is vendor-neutral. Here is how it maps onto a FortiGate at the edge, which is where many readers of this blog will be applying it. These are configuration directions, not a turnkey script; adapt interface and object names to your environment.
Virtual patching and IPS (Fingerprinting, Delivery, Trigger)
FortiGate’s IPS engine ships signatures for disclosed RCE CVEs, frequently before you can patch the affected product. Applying an IPS profile with an aggressive posture on the policy that publishes the service gives you virtual patching: the exploit is blocked in transit even while the vulnerable host is still unpatched.
config ips sensor
edit "edge-protect"
config entries
edit 1
set severity high critical
set status enable
set action block
next
end
set block-malicious-url enable
next
end
Attach the sensor to the inbound policy that serves the published application, and enable SSL inspection on that policy so the engine can see inside HTTPS. An IPS profile on an encrypted policy with no inspection sees only the TLS handshake.
WAF and proxy normalization (Delivery)
For published web applications, front them with the FortiGate inline WAF or an explicit/transparent proxy policy. Request normalization, size and encoding limits, and signature enforcement catch the anomalous-request structure described in Stage 3 without needing a signature for the specific CVE.
Egress filtering (Callback)
This is the control most environments are missing, and the one that most reliably turns a compromise into a non-event. From any server or DMZ segment, the outbound policy should be default-deny, permitting only known-good destinations and ports:
config firewall policy
edit 20
set name "dmz-egress-allowlist"
set srcintf "port-dmz"
set dstintf "port-wan"
set srcaddr "dmz-web-servers"
set dstaddr "approved-update-mirrors" "approved-api-endpoints"
set action accept
set service "HTTPS" "DNS"
set utm-status enable
set ips-sensor "edge-protect"
set application-list "block-proxy-and-tor"
set logtraffic all
next
edit 21
set name "dmz-egress-default-deny"
set srcintf "port-dmz"
set dstintf "port-wan"
set srcaddr "dmz-web-servers"
set dstaddr "all"
set action deny
set logtraffic all
next
end
The explicit deny with set logtraffic all is not decoration. Those deny logs are your highest-fidelity RCE alert: a DMZ web server trying to reach an address outside the allow-list means something on that server initiated a connection it never should have.
Outbound threat intelligence (Callback)
Layer DNS filtering and the botnet C2 / malicious-IP feeds on the egress policy so that callbacks to known infrastructure are blocked even if a destination slips through the allow-list, for example when an attacker abuses a legitimate cloud provider you have permitted.
config dnsfilter profile
edit "block-c2"
set block-botnet enable
config domain-filter
set domain-filter-table 1
end
next
end
Segmentation (Post-Exploitation)
Put internal segments behind the FortiGate (or use VDOMs / internal segmentation firewalling) so east-west traffic between the DMZ and internal zones is policied and inspected, not flat. A compromised DMZ host should not be able to reach a domain controller or a database VLAN without crossing an inspected policy that logs and limits it.
Verification and Validation
You do not validate these controls by waiting for a real attack. Validate them deliberately:
- IPS / virtual patching: From an authorized test host, confirm that a benign request to a protected endpoint is permitted and that the IPS logs show the sensor is attached and inspecting. Check
diagnose ipscounters and the IPS log for the policy. - Egress default-deny: From a server in the DMZ segment, attempt an outbound connection to an arbitrary external IP on an unusual port. It should fail, and the deny policy should log it. This is a two-minute test that proves the single most important control.
- SSL inspection coverage: Confirm the inbound policy that serves the application has SSL inspection enabled. Without it, IPS and WAF are inspecting an opaque tunnel.
- Logging reaches the SIEM: Verify that both IPS events and egress deny events are forwarded to FortiAnalyzer / your SIEM, and that someone has an alert on DMZ egress denials.
Troubleshooting and Gotchas
IPS is enabled but nothing is logged. The most common cause is SSL inspection missing on the policy, so the engine never sees cleartext. The second is the profile being set to monitor rather than block, or the relevant signature severity being excluded from the sensor. Confirm the sensor is attached to the correct policy and that the policy actually matches the traffic (check the policy hit counters).
Egress allow-list breaks legitimate updates. Appliances and servers often need to reach vendor update and licensing endpoints, which can sit behind rotating CDN IPs. Use FQDN address objects and, where the vendor supports it, internet service database (ISDB) objects rather than hardcoded IPs, so the allow-list tracks the real destinations without opening the segment wide.
Callback uses DNS or HTTPS to blend in. Default-deny on destinations handles the obvious cases, but C2 tunneled over permitted DNS or HTTPS needs the behavioral layer: DNS filtering with botnet blocking, outbound IPS, and SIEM analytics looking for beaconing intervals and abnormal query volume. If DNS is allowed outbound from servers directly, force it through an internal resolver you can inspect instead.
You patched, so you think you are done. Patching removes the trigger for known bugs. It does nothing for the next zero-day, and nothing for a host that was already compromised before you patched. The post-patch checklist is: confirm the fix, then hunt for the callback and persistence that may predate it. Egress logs and EDR are where you look.
The Takeaway
RCE is frightening because the exploit is instantaneous and often unauthenticated. But the attack that uses it is a chain with six links, and you only need to break one. The exploit you cannot see is bracketed by two things you can: the anomalous request that delivers it, and the outbound callback that makes it useful. Inspect the first with IPS and a WAF. Strangle the second with default-deny egress. Patch to delete the trigger, segment to contain the aftermath, and log every denial so the attempt that gets blocked still tells you someone tried.
Recent posts
-
-
DNS is one of those technologies that quietly underpins... Full Story
-
BGP issues on FortiGate firewalls usually trace back to... Full Story
-
Every time your laptop talks to your router, a... Full Story
-
If you've spent any time configuring NAT on a... Full Story
-
If you have spent any time configuring firewall policies... Full Story
-
High availability on FortiGate is one of those features... Full Story
-
If you've configured SD-WAN on a FortiGate, you've almost... Full Story
-
FortiLink is the management protocol that turns a FortiSwitch... Full Story
-
FortiSwitches are pretty rock solid from Mean Time Between... Full Story
-
This is a quicky tip. Have you ever gone... Full Story
-
DNS is one of those quiet pieces of internet... Full Story
-
This article is an updated version of the previous... Full Story
-
You will add ns2 as a secondary (slave) BIND9... Full Story
-
In the process of deploying my lab, I needed... Full Story
-
RFC 8805, used to be known as Self-Correcting IP... Full Story
-
Years back, I wrote an article about certificate pinning. ... Full Story
-
FortiGates have the ability to send alerts to Microsoft... Full Story
-
In this post, I am going to walk through... Full Story
-
Troubleshooting VoIP on a FortiGate can feel like trying... Full Story
-
Prior to FortiOS 7.0, there were three commands to... Full Story
-
In this post, I am going to go over... Full Story
-
What we are going to do: We are going... Full Story
-
Choosing between FGCP (FortiGate Clustering Protocol) and FGSP (FortiGate... Full Story
-
Creating a VLAN on macOS (The "Pro" Move) A... Full Story
-
This blog post explores the logic behind how macOS... Full Story
-
Pretty Fly for a Wi-Fi Tell My Wi-Fi Love... Full Story
-
Part of my daily gig is creating BoMs (Bill-of-Materials)... Full Story
-
ICMP introduces several security risks, but careful filtering, rate... Full Story
-
The command diag debug application dhcps -1 enables full... Full Story
-
In the world of FortiOS, execute tac report is... Full Story
-
LLDP; What is it The Link Layer Discovery Protocol... Full Story
-
What it actually does When you run diagnose fdsm... Full Story
-
Monkey Bites are bite-sized, high-impact security insights designed for... Full Story
-
I have run macOS in macOS with Parallels but... Full Story
-
Don't be confused with my other FortiNAC posts where... Full Story
-
This is the third session in a multi-part article... Full Story
-
Today I was configuring key-based authentication on a FortiGate... Full Story
-
Netcat, often called the "Swiss Army knife" of networking,... Full Story
-
At its core, IEEE 802.1X is a network layer... Full Story
-
In case you did not see the previous FortiNAC... Full Story
-
This is our 5th session where we are going... Full Story
-
Now that we have Wireshark installed and somewhat configured,... Full Story
-
The Philosophy of Packet Analysis Troubleshooting isn't about looking... Full Story
-
Plus what you actually get with FortiCare Essential, Premium... Full Story
-
No fluff. Just the config that works. This one... Full Story
-
No fluff. Just the config that works. Executive Summary... Full Story