Rich Gibbs

159 exploit probes hit my server in 24 hours. Here’s how I checked for signs that any succeeded.

server-security · apache · ssh · fail2ban · wordpress · vps · ec2 · incident-triage · indie-founder

Direct answer

Internet-facing servers receive automated exploit probes constantly. To distinguish scanner noise from a compromise, check the requested path together with its response status and body, review non-GET successes, inspect privileged SSH authentication logs, and look for resulting service or system changes.

I checked one of my public servers for the exact UTC window from 2026-08-07T08:28:35Z through 2026-08-08T08:28:35Z and found this:

  • 778 web requests
  • 221 source IPs
  • 159 exploit-style probes
  • 30 requests aimed at WordPress paths
  • zero 2xx responses in the suspicious-path count
  • zero recorded SSH events for accepted authentication, failed authentication, or invalid users

That path-based count was not the whole review. A separate pass over successful non-GET requests found four POST responses with 200, which I examine below.

The requests included WordPress installers, REST endpoints, and a malformed JSON-RPC login attempt.

That sounds dramatic until you run a server connected to the public internet for a while. Bots scan everything. They do not need to know who you are, what you built, or whether you even run WordPress. They work through IP ranges and domain lists, trying the same cheap paths everywhere.

The useful question is not, “Did a bot touch my server?”

It is, “What happened after it knocked?”

Here is the order I used to answer that without turning routine scanner noise into an imaginary incident.

A suspicious URL is a lead, not a verdict

The first pass counted requests whose paths looked like common automated probes. The list included patterns such as:

/wp-admin/
/wp-login.php
/xmlrpc.php
/?rest_route=/batch/v1
/.env
/.git/
/phpmyadmin/
/cgi-bin/
../
/etc/passwd

Those paths are worth flagging because they tell me what the scanner was looking for. They do not tell me whether the server had the vulnerable software or whether anything executed.

My sites do not run WordPress. A request for /wp-admin/install.php therefore tells me more about the scanner than it does about my stack.

I checked the response chain next. The WordPress probes were redirected and then ended at a missing page: 301 followed by 404. The malformed JSON-RPC login request received 400 Bad Request.

That is the normal shape of an unsuccessful automated scan: ask for something that is not there, then get redirected, rejected, or told it does not exist.

A 200 is not automatically a successful exploit

The part that deserved closer inspection was four non-GET requests that returned 200 OK:

  • one POST /
  • three POST /?rest_route=/batch/v1

A dashboard that only counts status codes could make these look like four successful attacks. That would be the wrong conclusion.

I compared each response with the normal homepage. Each was 25,681 bytes—the exact size of the normal homepage response—and inspection identified it as the normal homepage fallback. It was not a WordPress JSON response or a batch API result.

That supports the catch-all explanation. The response alone does not prove that no application code touched the request, so I also checked application and system evidence for execution or state changes.

This is why I do not use “malicious-looking request plus HTTP 200” as a compromise detector. A 200 means the web server produced a response. It does not prove that the requested component existed, accepted the payload, or executed it.

For any suspicious 2xx, I check:

  1. The method: GET, POST, PUT, or something stranger.
  2. The exact path and query string.
  3. The response body, content type, and size.
  4. Whether it matches a known normal page.
  5. Application logs for a corresponding handler or error.
  6. System evidence for authentication, execution, or persistence.

That is more work than coloring every 200 red, but it produces an answer I can defend.

I checked the real administrative door separately

Public web probes and SSH access are different questions. I reviewed the privileged sshd journal rather than relying only on what an unprivileged dashboard could see.

On this RHEL-family host, the basic check is:

sudo journalctl -u sshd --since "24 hours ago" --no-pager

I looked for accepted authentication, failed passwords or keys, and invalid usernames. The result for the same window was:

Accepted logins: 0
Failed logins:   0
Invalid users:   0

Fail2Ban was active, its sshd jail was healthy, and it had no failures or bans to report. That is consistent with no SSH authentication attempts being recorded during the window. A zero ban count is not suspicious when there were also zero failed attempts.

Useful checks on a system using Fail2Ban are:

sudo fail2ban-client status
sudo fail2ban-client status sshd

The lesson is simple: do not infer SSH activity from web logs, and do not infer web exploitation from SSH logs. Check each control plane using the source that actually records it.

Then I looked for impact

Logs can show a bad request being rejected. They can also miss context. I wanted to know whether the host itself had changed or degraded around the same time.

I checked for failed services, Apache errors, unexpected listeners, and evidence of a new administrative session:

sudo systemctl --failed --no-pager
sudo journalctl -u httpd --since "24 hours ago" --no-pager
sudo ss -ltnp
sudo last -ai

The host had zero failed systemd units. Apache’s journal had no fatal errors or exploit-success indicators. The firewall and Fail2Ban were active. The expected public sites still returned their normal content through their real HTTPS paths.

That gave me a coherent result:

  • the scanners sent recognizable commodity payloads;
  • the suspicious paths did not return suspicious success bodies;
  • there was no SSH authentication activity;
  • there was no accompanying service failure or observed persistence signal.

Taken together, that is evidence that these observed probes were unsuccessful. It is not proof that no compromise occurred through another path, that the logs were complete, or that the server cannot be compromised later.

What I deliberately did not do

I did not start blocking 221 individual IP addresses.

Most commodity scanners rotate through hosting providers, proxies, and compromised devices. Chasing every address manually creates a long denylist and very little durable protection. I would block or rate-limit a source when it is sustained, abusive, or hitting an expensive endpoint—not merely because it asked for /wp-login.php once.

I did not install another security product halfway through the investigation. Changing the system before understanding the logs can destroy useful context and create fresh noise.

I did not reboot the server, rotate credentials without evidence, or declare an incident. Those actions have a cost. They belong after a finding, not before one.

I also did not declare the host “unhackable.” A clean 24-hour review only describes the evidence available in that window. Logs can be incomplete. Application-level authentication may live elsewhere. A sophisticated attacker with root access may tamper with local evidence. Important systems should send logs off-host and retain enough history to investigate beyond a single day.

Scope-honest beats confident theater.

The small-server triage order

For a solo founder or small team, this is the boring sequence I would use when a dashboard says bots are probing the server:

  1. Confirm the window. Record exact start and end times.
  2. Count the requests and sources. Volume provides context, not a verdict.
  3. Classify suspicious paths. WordPress, environment files, admin panels, traversal, and shell probes are common buckets.
  4. Inspect response chains. A redirect into a 404 is different from a real application response.
  5. Review every suspicious 2xx. Compare body, size, content type, and application handling.
  6. Separate web traffic from authentication. Read the privileged SSH logs directly.
  7. Look for impact. Failed units, unknown listeners, new users, changed jobs, application errors, and unexpected outbound traffic matter more than scary URLs.
  8. Preserve the evidence. Avoid changing the box until you understand what happened.
  9. Fix controls, not feelings. Close exposed services, patch real vulnerabilities, tighten authentication, and improve logging based on findings.

The order matters. If I jump straight from “someone requested /wp-admin” to “the server is breached,” I create work without improving security. If I dismiss every scan as harmless, I risk missing the one response that was different.

The boring summary

In this window, 159 exploit-style probes reached the web server. None produced a response indicating exploit success. Four POST requests returned 200, but inspection showed the normal static homepage fallback rather than a WordPress or batch API response. SSH recorded no accepted authentication, failed authentication, or invalid users. I found no related service impact in the evidence reviewed.

The probes arrived, but I found no evidence in this 24-hour review that any of them succeeded.

This kind of read-only triage is part of what I look at in a VPS/EC2 Hardening QuickCheck: what is exposed, what the available logs show, and which findings deserve action first. It is a posture review, not a penetration test, compliance certification, or guarantee that a server is secure.

The goal is not a dashboard with fewer scary numbers. The goal is knowing which number requires a wrench.

— Rich

Frequently asked questions

Do WordPress probes mean my server was hacked?

No. Requests for wp-admin, wp-login.php, xmlrpc.php, or WordPress REST routes usually mean an automated scanner found your IP or domain. The important evidence is how the server responded and whether the request caused authentication, code execution, file changes, or other impact.

Does an HTTP 200 response prove an exploit succeeded?

No. A catch-all route, redirect target, or static homepage can return 200 for a nonexistent or malicious-looking path. Compare the response body and size with the normal page, then check application and system logs for execution or state changes.

What should I check after suspicious traffic hits a VPS?

Review the request path, method, response chain, successful non-GET requests, privileged SSH authentication logs, failed services, listening ports, recent configuration changes, scheduled jobs, and application errors. Keep the investigation read-only until you understand the evidence.