Skip to main content

Command Palette

Search for a command to run...

Why most vulnerability scans miss what matters: A practitioner's take

There's a huge gap between what a scanner catches and what an attacker can actually exploit.

Updated
Why most vulnerability scans miss what matters: A practitioner's take
P
Cybersecurity and infrastructure specialist with 15+ years in enterprise environments, SIEM operations, incident response, Linux hardening, and vulnerability assessment.

I've run a ton of vulnerability scans over the years. Automated, manual, or using tools like WPScan and Nessus to do the heavy lifting. After a while, you start to notice something uncomfortable: the things that actually get exploited in real incidents are usually the ones the scanner missed, flagged as a low priority that got buried, or just couldn't see at all.

Don't get me wrong, this isn't a knock on the tools, they’re useful and you should definitely use them. But there's a huge gap between what a scanner catches and what an attacker can actually exploit. Scanners only know about signatures, CVEs, and common misconfigurations. Attackers exploit context, they find the sweet spot where your specific configuration, weak access controls, and monitoring blind spots collide.

This article is about that gap. What scanners miss, why they miss it, and how to complement automated scanning with the kind of manual analysis that actually reflects your real exposure.


Table of Contents

  1. What a vulnerability scanner actually does

  2. The seven categories of what gets missed

  3. Logic flaws and business context

  4. The severity rating problem

  5. Manual checks that change the picture

  6. Building a practical assessment workflow

  7. Communicating findings that actually get fixed

  8. What a good scan report looks like vs. what most look like


1. What a vulnerability scanner actually does

If you want to understand why scanners fall short, you have to look at what they actually do under the hood. A vulnerability scanner is essentially just a super-fast pattern matcher. It talks to a target, asks it specific questions, and checks if the answers match any known vulnerabilities, bad configurations, or old software versions in its database.

That's genuinely powerful. A good scanner can check thousands of conditions in minutes that would take a human analyst hours. But pattern matching against a known database is a fundamentally different thing from understanding your environment.

Scanners just scratch the surface. A real security pro looks deeper and starts connecting the dots, asking what an open port means for this specific application, or what happens when you chain a low-severity flaw with a messy sudo rule. They ask: what is the actual impact here?

Automated scans simply can't capture that layer of context, but that's exactly where the real, high-impact risks live.


2. The seven categories of what gets missed

Over time I've noticed that scanner gaps tend to fall into repeating categories. Not random misses but patterns, just like the WordPress compromise patterns in the previous article.

Understanding the categories helps you know where to direct manual attention after a scan.

Gap 1: Version string absence or spoofing

Scanners identify vulnerable software primarily by matching version strings in banners, headers, and responses. If a version string is absent, suppressed, or incorrect, the scanner either skips the check or misidentifies the software entirely.

# A scanner sees this in an HTTP response header:
# Server: Apache

# Without the version, it can't match against CVE-2021-41773 (Apache 2.4.49)
# The service might still be running that exact version

# Check the actual version manually:
curl -I https://yoursite.com | grep -i server
apache2 -v
nginx -v

# Check what version information is being disclosed
curl -s -I https://yoursite.com | grep -iE "server|x-powered-by|x-generator"

The irony: hardening guides tell you to suppress version strings to reduce information disclosure. That's correct advice. But it also means a scanner that relies on version strings will miss vulnerabilities in hardened systems. Always verify versions manually during a real assessment.

Gap 2: Findings that only make sense in context

A scanner flags open port 3306 (MySQL) with a low-severity note: "database port is accessible." Technically accurate, unremarkable by itself. But in context: this is a WordPress site, the MySQL instance is accessible from the public internet, and the WordPress database user has GRANT ALL PRIVILEGES. Now it's critical. The scanner saw the port. It couldn't see what the port means in this specific deployment.

Gap 3: Chained vulnerabilities

This is the most underappreciated gap. Individual findings that are genuinely low severity can chain into a critical exploit path. A scanner scores them independently and they both end up buried at the bottom of the report:

  • Finding A (Low): Directory listing enabled on /backup/

  • Finding B (Low): Backup files include database dump with .sql extension

Individually low. Together: an unauthenticated attacker can download a complete database dump containing credentials, PII, and password hashes. Combined severity: Critical. The scanner didn't miss either finding, it missed the relationship between them.

Gap 4: Post-authentication vulnerabilities

Most automated scans run unauthenticated, they probe the external surface without logging in. This means anything that requires a valid session is invisible: privilege escalation within the application, IDOR (Insecure Direct Object References), broken access control between user roles, and admin-level misconfigurations.

For web applications, authenticated scanning covers some of this, but it requires configuration and account setup that most teams skip because it's friction. In a real assessment, authenticated testing is not optional, it's where the most impactful findings tend to live.

Gap 5: Configuration semantics vs. syntax

A scanner can check whether a configuration directive is present. It generally can't check whether the configuration is logically correct for the environment.

# Scanner check: is fail2ban running?
systemctl is-active fail2ban

# What the scanner can't verify:
# Is fail2ban actually watching the right log file?
# Does the log format match the filter regex?
# Is the SSH port in the jail config the custom port you're using?
# Has the jail ever actually banned anyone?

# Manual check — confirm fail2ban is actually working:
fail2ban-client status sshd

# Verify the filter matches your actual log format:
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf \
  --print-all-matched | tail -20

A Fail2ban configuration that's syntactically valid but watching the wrong log file provides zero protection while giving you a false sense of security. That's worse than not having it configured at all, at least then you know you're unprotected.

Gap 6: Custom and third-party code

CVE databases cover known software. Custom code, the plugin your developer wrote, the integration someone built three years ago, has no CVE entry because no one has analyzed it publicly. Scanners have nothing to match against.

This is where manual code review and manual testing become the only option. For WordPress specifically, custom plugins and themes are the most common location for undiscovered vulnerabilities in real client environments.

Gap 7: Out-of-scope infrastructure

Scans have a defined target scope. What's outside that scope is invisible, the staging server on a subdomain that shares database credentials with production, the legacy API endpoint that was "deprecated" but still runs, the third-party service with excessive access that nobody thought to include in the scope.

Attackers don't respect scope definitions. They enumerate everything. A practitioner doing a thorough assessment needs to at least enumerate what exists outside the defined scope, even if they don't test it, so the client understands the full picture.


3. Logic flaws and business context

Logic flaws deserve their own section because they’re the perfect example of the gap between what automated tools can catch and what actually matters. We're not talking about a buffer overflow or a known CVE here.

A logic flaw is an issue with how an app handles its own business rules. It’s a case where the code does exactly what it was programmed to do, it’s just that what it was programmed to do is fundamentally insecure.

Classic examples that no scanner will ever catch:

  • A password reset flow that accepts any valid token in the system, not just the one sent to the requesting user

  • An e-commerce checkout that applies a discount code after the price validation, allowing negative totals

  • An API endpoint that returns all records when the user ID parameter is set to null instead of rejecting the request

  • A file upload that validates MIME type client-side but not server-side, change the Content-Type header and upload a PHP file

# Manual testing for IDOR (Insecure Direct Object Reference)
# Example: can user 1001 access user 1002's data?

# Test with your own authenticated session:
# Change the user ID in the request to another user's ID
curl -s -H "Cookie: session=YOUR_SESSION" \
  https://yourapp.com/api/user/1002/profile

# If you get a 200 with user 1002's data instead of a 403 — that's an IDOR
# No scanner will find this because it requires understanding the data model

# Test for mass assignment vulnerability
# Does the API accept fields it shouldn't?
curl -s -X PUT \
  -H "Content-Type: application/json" \
  -H "Cookie: session=YOUR_SESSION" \
  -d '{"username":"testuser","email":"test@test.com","role":"admin"}' \
  https://yourapp.com/api/user/1001/update

These require a human who understands the application's intended behavior and can identify where the implementation diverges from it. No amount of scanner configuration closes this gap.


4. The severity rating problem

CVSS scores are great for comparing vulnerabilities on paper, but they’re a terrible guide for prioritizing them in a real-world environment. A CVSS 9.8 flaw in a service that’s firewalled off and protected by compensating controls is way less urgent than a CVSS 5.0 vulnerability in a public-facing app with zero mitigations and known active exploitation in the wild.

Most scan reports present findings sorted by CVSS score without adjustment for environmental context. This is how you end up with a 40-page report where the client spends two weeks patching Medium findings while the actual critical exposure stays open.

The practitioner's job is to apply that environmental context, to translate "CVSS 9.8" into "this is either a drop-everything-now or a next-cycle issue depending on what's around it."

A practical prioritization framework

When reviewing scan output, ask four questions per finding before assigning priority:

  1. Is it reachable? Can an attacker actually reach this service or component from their starting position?

  2. Is it exploitable right now? Is there a working public exploit, or is it theoretical?

  3. What's the blast radius? If exploited, what access does it give? Read-only data? Admin credentials? Root?

  4. Are there compensating controls? WAF, network segmentation, monitoring, rate limiting, do any of these meaningfully reduce the risk?

# Quick reachability check, is this service actually accessible externally?
# Run from a machine OUTSIDE your network (VPS, cloud instance)

# Check if a specific port is reachable
nc -zv YOUR_SERVER_IP 3306 2>&1
# "Connection refused" or timeout = not reachable externally = lower priority
# "Connected" = reachable = high priority regardless of CVSS

# Check what your server looks like from the outside
nmap -sV -p- --open YOUR_SERVER_IP

# For web: check response headers for security controls
curl -s -I https://yoursite.com | grep -iE \
  "x-frame-options|content-security-policy|strict-transport|x-xss|x-content-type"

5. Manual checks that change the picture

These are the checks that consistently reveal findings automated scanners miss. Run these after every automated scan, on every engagement, and the combination will give you a much more accurate picture of actual exposure.

Check 1: Enumerate users across all access paths

# Local system users with login shells
grep -v '/usr/sbin/nologin\|/bin/false\|/bin/sync' /etc/passwd | \
  awk -F: '{print \(1, \)3, $7}'

# Who has sudo access?
grep -Po '^sudo.+:\K.*$' /etc/group | tr ',' '\n'
getent group sudo wheel

# Users with SSH keys configured (each of these is an access path)
find /home /root -name "authorized_keys" -exec echo "=== {} ===" \; \
  -exec cat {} \; 2>/dev/null

# Active sessions right now
w
last | head -20
lastb | head -20    # failed login attempts

Check 2: Map all running services and their permissions

# Everything listening on a network socket
ss -tlnp
ss -ulnp    # UDP as well

# Cross-reference with what UFW is allowing
ufw status verbose

# Processes running as root that don't need to be
ps aux | awk '\(1=="root" {print \)1, \(11, \)12}' | sort -u

# Services enabled at boot (potential forgotten services)
systemctl list-unit-files --type=service --state=enabled | grep -v systemd

Check 3: Audit file permissions beyond the obvious

# SUID binaries, these run as owner regardless of who executes them
find / -xdev -perm -4000 -type f 2>/dev/null | sort
# Compare against known-good list, any unexpected additions are suspicious

# World-writable directories (potential staging areas)
find / -xdev -type d -perm -0002 2>/dev/null | grep -v proc | grep -v sys

# Files modified in the last 24 hours in sensitive paths
find /etc /usr/bin /usr/sbin /var/www -mtime -1 -type f 2>/dev/null

# Config files with world-readable sensitive data
find /etc -name "*.conf" -readable -exec grep -l "password\|secret\|key\|token" {} \;

Check 4: Validate cryptographic configuration

# Test TLS configuration quality (run from external machine)
# Option 1: command line
openssl s_client -connect yoursite.com:443 2>/dev/null | \
  grep -E "Protocol|Cipher"

# Option 2: use testssl.sh (comprehensive TLS audit)
bash testssl.sh --severity HIGH yoursite.com

# Check SSH cryptographic algorithms in use
ssh -vvv -p 2222 user@yourserver 2>&1 | \
  grep -E "kex|cipher|mac|host key"

# What ciphers does the SSH server offer?
nmap -p 2222 --script ssh2-enum-algos yourserver

Check 5: Review cron jobs for privilege abuse

# System-wide cron jobs
cat /etc/crontab
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/

# Per-user cron jobs (check all users)
for user in $(cut -d: -f1 /etc/passwd); do
  crontab -u \(user -l 2>/dev/null && echo "^^^ \)user"
done

# Systemd timers (modern replacement for cron — often overlooked)
systemctl list-timers --all

# Look for cron jobs calling scripts in world-writable directories
grep -r "/tmp\|/var/tmp" /etc/cron* /etc/crontab 2>/dev/null

Check 6: Database exposure audit

# Is MySQL/PostgreSQL listening on a public interface?
ss -tlnp | grep -E "3306|5432"
# Should show 127.0.0.1:3306, not 0.0.0.0:3306

# MySQL: check user privileges (run as root or DBA)
mysql -u root -e "SELECT user, host, plugin FROM mysql.user;"
mysql -u root -e "SHOW GRANTS FOR 'wordpress_user'@'localhost';"

# Are there anonymous MySQL users? (classic misconfiguration)
mysql -u root -e "SELECT user, host FROM mysql.user WHERE user='';"

# Check for test databases left over from installation
mysql -u root -e "SHOW DATABASES;" | grep -E "test|demo|backup"

Check 7: Web application headers and information disclosure

# What is your site giving away about its stack?
curl -s -I https://yoursite.com | grep -iE \
  "server|x-powered-by|x-generator|x-aspnet|x-runtime|via"

# Are development or debug endpoints exposed?
for path in /phpinfo.php /.env /config.php /debug /api/debug \
            /server-status /server-info /.git/config /backup.sql \
            /wp-config.php.bak /adminer.php /phpmyadmin; do
  code=\((curl -s -o /dev/null -w "%{http_code}" https://yoursite.com\)path)
  [ "\(code" != "404" ] && [ "\)code" != "444" ] && \
    echo "WARNING: \(path returned HTTP \)code"
done

# Check for exposed .git directory (source code disclosure)
curl -s https://yoursite.com/.git/config | grep -i "repositoryformatversion"
# Any output here means your source code is publicly accessible

6. Building a practical assessment workflow

The goal is a workflow that combines automated scanning with targeted manual checks in a way that's repeatable and produces findings you can actually act on. This is roughly the process I follow on WordPress and VPS security assessments.

The tools that actually earn their keep

A quick note on tooling: the best tools are the ones you understand well enough to interpret their output accurately. A Nessus scan run by someone who doesn't understand what they're looking at produces a report with 200 findings that no one prioritizes correctly. The same environment scanned manually by someone who knows the context produces ten findings that actually get fixed.

Tool What it's genuinely good for What it misses
Nmap Port mapping, service identification, NSE scripts Application-layer logic, auth issues
WPScan WordPress plugin/theme versions, known CVEs, user enumeration Custom code, chained findings
Nikto HTTP headers, common misconfigs, version disclosure Auth-required content, logic flaws
OpenVAS / Nessus Broad CVE coverage, network service vulns Web app logic, business context
testssl.sh Deep TLS/SSL analysis Everything that's not TLS
Lynis Local system hardening audit Network-facing exposure
# A practical scan sequence for a VPS + WordPress target

# 1. External port scan (from outside the network)
nmap -sV -sC -p- --min-rate 1000 -oN nmap_full.txt TARGET_IP

# 2. Web technology fingerprinting
whatweb -a 3 https://targetsite.com

# 3. WordPress-specific audit
wpscan --url https://targetsite.com \
  --enumerate p,t,u,vp \
  --plugins-detection aggressive \
  --api-token YOUR_TOKEN \
  -o wpscan_output.json \
  --format json

# 4. HTTP security headers
curl -s -I https://targetsite.com | grep -iE \
  "strict|content-security|x-frame|x-content|referrer|permissions"

# 5. TLS configuration
testssl.sh --severity MEDIUM --quiet https://targetsite.com

# 6. Local system audit (run on the server with access)
lynis audit system --quiet --log-file lynis_output.log

7. Communicating findings that actually get fixed

This is the part most technical people overlook—and it’s exactly why most security assessments fail to deliver real value. A finding that isn't communicated clearly simply doesn't get fixed. A report that buries critical issues under 80 pages of scanner noise doesn't get read.

The gap between 'finding identified' and 'finding fixed' is almost always a communication problem, not a technical one.

The anatomy of a finding that gets fixed

Every finding in a useful report needs five elements:

1. Clear title with severity: not "CVE-2023-XXXX" but "Unauthenticated RCE in public-facing Apache — High"

2. Plain-language impact statement: what can an attacker actually do? Avoid jargon. "An attacker can read all files on the server including database passwords" is better than "Remote code execution allows arbitrary command execution."

3. Evidence: show the finding, don't just describe it. A curl output, a screenshot, an nmap result. This also protects you — it proves the finding existed at the time of assessment.

4. Remediation with effort estimate: don't just say "update the software." Give the exact command, the expected time to implement, and any caveats (test in staging first, requires maintenance window, etc.)

5. Verification criteria: how will the client know the fix worked? This is often omitted and it's one of the most valuable things you can include.

## Finding: WordPress admin user enumeration via REST API — Medium

**Severity:** Medium  
**Effort to fix:** 15 minutes  
**Affected component:** WordPress REST API — /wp-json/wp/v2/users

### What an attacker can do
Anyone can visit `https://yoursite.com/wp-json/wp/v2/users` and get a list of all 
WordPress usernames. Combined with a brute force tool against wp-login.php, this 
eliminates the username discovery step and makes credential attacks faster 
and more targeted.

### Evidence

curl -s https://yoursite.com/wp-json/wp/v2/users | python3 -m json.tool [ { "id": 1, "name": "admin", "slug": "admin" } ]


### How to fix it
Add the following to your theme's functions.php or a site-specific plugin:

```php
add_filter('rest_endpoints', function($endpoints) {
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    return $endpoints;
});

How to verify it's fixed

After applying the fix, visiting https://yoursite.com/wp-json/wp/v2/users should return a 404 response. Confirm with: curl -s -o /dev/null -w "%{http_code}" https://yoursite.com/wp-json/wp/v2/users Expected output: 404


8. What a good scan report looks like vs. what most look like

Let’s be honest: most automated scan reports are completely useless. They're just data dumps. Sorting 150 findings by CVSS score and emailing over a PDF isn't a security assessment, it's just a way to shift liability. The client gets a paper trail to cover themselves if things go sideways, and the consultant gets paid, but nothing actually becomes more secure.

A useful report has a different structure entirely:

The executive summary is where most people get it wrong. Too many engineers write them for other engineers, overloading the page with acronyms and CVSS scores. In reality, the people reading it are business owners, managers, or board members who just need to make a budget decision. Speak their language. Answer what they actually care about: How bad is the situation, and what do we do next?

A good executive summary is just three paragraphs: current risk level, the top three findings in plain language, and an actionable timeline for next steps. That's it.


At the end of the day, the difference between running a scan and doing a real assessment is the difference between data and insight. Scanners hand you data. Assessments hand you understanding, your actual exposure, what an attacker would exploit, and what it takes to actually lower your risk instead of just clearing a checklist.

Tools are a requirement, but they are never the whole story. A security professional’s real value comes alive after the scan is done: in the context, the chaining, the prioritization, and the communication. That’s what matters, and that’s exactly what a script can’t replicate.


References


Written by Pablo Huertas - Cybersecurity Specialist & Technical Writer
15+ years in enterprise technical support and incident management.

More from this blog

P

Pablo Huertas - Cybersecurity & Infrastructure Notes

4 posts

Cybersecurity and infrastructure specialist with 15+ years in enterprise environments, SIEM operations, incident response, Linux hardening, and vulnerability assessment.

I write practical notes from real-world experience: how systems actually break, why incidents really happen, and what works when theory isn't enough.

Available for technical writing and selective consulting.

📍Located in Guatemala - English and Spanish