Our external scan says this server is vulnerable because of its version. Is it? Service fingerprinting, version banners, backports and what an outside check can conclude

DedSec Security Research ·

Take an invented case. An external scan reports a critical finding against ssh.example.com: OpenSSH 9.2p1, affected by CVE-2024-6387, remote code execution as root. The server runs Debian 12 and was patched in July 2024. The owner says it is a false positive. The scanner says the version is in the affected range. Both are reading the same few dozen bytes, and neither has looked at what those bytes can and cannot prove.

A version-based finding is a chain of inferences. Some software sent a string. A signature turned the string into a product and a version. The version became a CPE, the CPE was compared with an advisory's affected range, and the range produced a CVE. Each link loses information, and the one that decides this case, the distribution's package revision, is dropped somewhere in the middle. This post goes through the chain: how versions are observed from outside, why the observed version is wrong in both directions, a real CVE worked through against real distribution records, and what an outside check can honestly conclude.

Neighbouring questions have their own posts: how advisory databases match packages and versions, which covers CPE naming, version ordering and backports inside packages in depth; why a finding needs the evidence that produced it; CVSS, EPSS and KEV for what to fix first once a finding is real; whether a host is yours at all; TLS drift and security headers for the other things an outside check reads from the same responses.

Sources, all retrieved on 24 October 2026: RFC 4253 section 4.2 and RFC 9110 sections 7.6.3 and 10.2.4 from rfc-editor.org; the nginx ngx_http_core_module documentation; the Apache HTTP Server 2.4 core documentation; the PHP manual's core php.ini directives and php.ini-production in the php-src repository; the Nmap book chapters "Technique Described" and "nmap-service-probes File Format" and the nmap-service-probes file from svn.nmap.org; the README of Salesforce's JARM project; the Qualys advisory for CVE-2024-6387 and the OpenSSH 9.8 release notes; Debian's openssh packaging on salsa.debian.org (debian/rules, package-versioning.patch, debian-banner.patch), the bookworm sshd_config(5) manual page, the Debian security FAQ and the Debian security tracker entry for CVE-2024-6387; the Ubuntu CVE tracker's JSON record; Red Hat's "Backporting Security Fixes" page and its security data API record for the CVE; the NVD CVE API 2.0 record; and CISA's Known Exploited Vulnerabilities catalog, version 2026.10.23. Tracker and NVD records change; the values below are as retrieved. Host names use example.com, and banners marked illustrative were derived from the packaging rules, not captured from a live host.

How a version is observed from outside

The SSH identification string

RFC 4253 section 4.2 requires both sides of an SSH connection to send an identification string before anything else, in the form "SSH-protoversion-softwareversion SP comments CR LF". The protoversion must be "2.0". The comments field is optional and, if present, is separated from softwareversion by a single space. The whole line is limited to 255 characters, and the RFC's own example is "SSH-2.0-billsSSH_3.6.3q3". The softwareversion "is primarily used to trigger compatibility extensions and to indicate the capabilities of an implementation"; the comments "SHOULD contain additional information that might be useful in solving user problems."

Two details matter for scanning. The server sends this line unprompted, so a scanner only has to connect and listen. And the RFC says the identification string is used in the Diffie-Hellman key exchange, so a middlebox that rewrote it in transit would break the handshake; to change it, a device has to terminate the SSH session itself. The string reaching you came from whatever completed the handshake: the server, a bastion, or an SSH-aware proxy.

What OpenSSH puts in it depends on who built it. Debian's openssh packaging, whose debian/rules also carries the Ubuntu build logic, adds the package revision. Its debian/rules sets SSH_EXTRAVERSION to the distribution vendor name, a hyphen, and the Debian version with everything up to the last hyphen removed. package-versioning.patch then appends that, after a space, to the upstream release, giving lines of the form "SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u3" (illustrative). The patch gives its reason: "This makes it easier to audit networks for versions patched against security vulnerabilities." Under RFC 4253's grammar, the revision is in the comments field, not in softwareversion. Two details follow from the rule: the epoch at the front of the Debian version (the "1:" in 1:9.2p1-2+deb12u3) never appears in the banner, and a companion patch, debian-banner.patch, adds a DebianBanner option whose default is yes. Set it to no and sshd sends "SSH-2.0-OpenSSH_9.2p1" with no revision at all. Separately, upstream's VersionAddendum option appends arbitrary text to the banner.

HTTP Server and X-Powered-By

RFC 9110 section 10.2.4 says an origin server "MAY generate a Server header field", made of product identifiers with optional versions and comments, and that it "SHOULD NOT generate a Server header field containing needlessly fine-grained detail". What a server actually sends is a configuration choice:

Every one of these is a value the operator can shorten or remove, and in the commercial nginx case, replace with anything.

Protocol behaviour, including TLS

Where nothing announces a version, behaviour can still narrow it: which protocol versions and options are offered, how malformed input is answered, the order of fields, which error pages appear. JARM is a published example for TLS. Its README describes sending 10 crafted TLS ClientHello packets and hashing attributes of the ServerHello replies into a 62-character fingerprint, which can "group disparate servers on the internet by configuration" and "identify default applications or infrastructure". The README's own example table shows salesforce.com and force.com with one fingerprint, and google.com, youtube.com and gmail.com with another. That is the limit as well as the use: a TLS fingerprint describes whatever terminates TLS, and behind a CDN that is the CDN.

Nmap service detection: probes, match and softmatch

The Nmap book documents one well-described method. After a port scan, Nmap connects to each open TCP port and listens for about five seconds: the "NULL probe", because services such as SSH, SMTP and FTP announce themselves unprompted. Any reply is compared with roughly 3,000 NULL-probe signatures. If nothing matches, Nmap sends probe strings from its nmap-service-probes file, starting with the probes registered for that port, and compares the replies with each probe's signatures. A probe that detects TLS makes Nmap reconnect over TLS and restart detection on what is behind it.

A signature is either a match or a softmatch. A match identifies the service and can extract version fields with p/ (product), v/ (version), i/ (extra information), h/ (host name), o/ (operating system), d/ (device type) and one or more cpe:/ templates. A softmatch identifies only the service type, never a version, and scanning continues with probes known to recognise that type. Two consequences follow. A version in a scan report means some regular expression captured part of a reply; the reliability of the version is the reliability of that expression. And the o/ field is the service's claim about its operating system, which the documentation notes "may legitimately be different" from stack-based OS detection, for example when NAT forwards a port to a different machine.

Favicons, default pages and static assets

Web fingerprinting also uses content: a hash of the favicon compared with a table of known products' icons, the title or markup of a vendor's default page, characteristic paths, and version numbers in static asset names or query strings. These identify a product family well and a version poorly. An icon or a default page can stay the same across releases, and a versioned JavaScript file names the version of that file, which may have been copied into the application and says nothing about the server.

Observed fact versus inference

Every technique above produces one fact and several inferences. The fact is the bytes received from a given address and port, with the probe that elicited them, at a given time. "This host runs OpenSSH 9.2p1" is an inference from those bytes through a signature. "This host is affected by CVE-2024-6387" is an inference from that inference through an advisory. A report that does not keep the three apart asks you to dispute a conclusion without showing you the observation.

Why a version string is wrong in both directions

Backports make the version too old

Distributions with stable releases fix security issues by patching the version they shipped rather than moving to a new upstream release. Red Hat's page on backporting says it plainly: "just looking at the version number of a package will not tell them if they are vulnerable or not", and "some security scanning and auditing tools make decisions about vulnerabilities based solely on the version number of components they find. This results in false positives as the tools do not take into account backported security fixes." Debian's security FAQ answers the question "The version number for a package indicates that I am still running a vulnerable version!" with the same mechanism: fixes are backported to the version in the stable release, and the way to check is to compare the exact package version with the one in the security advisory.

So the upstream part of a version string can stay inside an advisory's affected range for the whole life of a release while the package is fixed. Only the distribution's revision, if the banner carries it, separates fixed from unfixed. The advisory-matching post covers the same effect for language packages installed by a distribution.

Edited or absent banners make the version unknowable, or wrong

Every source above is configurable. A banner can be shortened (ServerTokens Prod, server_tokens off, DebianBanner no), extended (VersionAddendum), removed, or replaced with text that names another product. Deliberate decoys exist too. A missing version is not evidence of a current version, and a present one is a claim, not a measurement.

The shortened case is the dangerous one for false positives. Debian sshd with DebianBanner set to no sends exactly what an unpatched upstream build of the same version sends. Nothing in the string can tell them apart.

Something else answered

The address a name resolves to is not necessarily the machine that runs the application:

The result can go either way. A current edge in front of an old origin hides the origin's version, and an origin that is not reachable except through the edge may not be exposed in the way the edge's version suggests.

The vulnerable component or condition is not what the banner names

A banner names one product. A vulnerability lives in a component, under conditions:

Worked example: CVE-2024-6387 (regreSSHion)

What upstream says

The Qualys advisory describes a signal handler race in sshd: if a client does not authenticate within LoginGraceTime seconds (120 by default), sshd's SIGALRM handler calls functions that are not async-signal-safe, such as syslog(). It is a regression of CVE-2006-5051, reintroduced in OpenSSH 8.5p1 when a 2020 logging change removed a guard from sigdie(). The advisory gives three ranges: OpenSSH before 4.4p1 is vulnerable if it was not backport-patched against CVE-2006-5051, or not patched against CVE-2008-4109, an incorrect fix for it; 4.4p1 up to but excluding 8.5p1 is not vulnerable; 8.5p1 up to but excluding 9.8p1 is vulnerable again. It is exploitable remotely on glibc-based Linux, where syslog() calls malloc() and free(); Qualys had not investigated other C libraries, and OpenBSD is not vulnerable. If sshd cannot be updated, setting LoginGraceTime to 0 removes the code execution risk at the cost of exposure to a denial of service by exhausting MaxStartups connections.

The OpenSSH 9.8 release notes, published on 1 July 2024, state the range as "Portable OpenSSH versions between 8.5p1 and 9.7p1 (inclusive)". Successful exploitation had been demonstrated on 32-bit Linux/glibc systems with ASLR, taking on average 6 to 8 hours of continuous connections under lab conditions; exploitation on 64-bit systems was "believed to be possible but has not been demonstrated at this time", and on non-glibc systems "conceivable but has not been examined". As of KEV catalog version 2026.10.23, CVE-2024-6387 is not listed, and the NVD record retrieved carries no KEV date. Neither fact says the bug is safe to leave; they say what is and is not on record.

What the distributions say

Every fixed package above keeps an upstream version inside 8.5p1 to 9.7p1.

What each banner lets you conclude

Using the Debian packaging rule above, with banners marked illustrative:

Where CPE drops the deciding detail

The step from fingerprint to CVE uses CPE, and both ends of this match lose information.

On the fingerprint side, the Debian and Ubuntu match lines in nmap-service-probes capture the revision for display (version "9.2p1 Debian 2+deb12u3", say) but emit the CPE as cpe:/a:openbsd:openssh: followed only by the upstream version. Any tool that hands that CPE to a matcher has discarded the revision before matching starts.

On the advisory side, NVD's configuration for CVE-2024-6387 as retrieved matches cpe:2.3:a:openbsd:openssh with versionStartIncluding 8.6 and versionEndIncluding 9.8, plus separate entries for version 8.5 with update p1, version 8.6 with update "-", version 4.4 with update "-", and every version before 4.4. Two things follow. NVD splits "9.2p1" into version 9.2 and update p1, while Nmap's CPE carries 9.2p1 in the version field, so a matcher has to split it correctly before comparing. And a range that includes 9.8 at its end, compared with a version of 9.8 from a "9.8p1" banner, reports the release that contains the fix. The configuration also has no notion of a Debian or Ubuntu revision. The only data that does is the distribution's own tracker.

The advisory-matching post covers CPE naming and its mapping problems generally. For outside checks the point is narrower: a CPE carries a product and an upstream version, and a backport is recorded in neither.

What an outside check can conclude

Findings from version evidence can be graded by the strongest observation behind them:

  1. Banner-inferred. A version was read from a banner or header and is inside an advisory's range. The weakest grade, and the honest label for anything without a distribution revision.
  2. Config-inferred. The version evidence plus something observable about configuration: a distribution revision compared with that distribution's tracker, a module or endpoint whose presence is visible, a protocol option offered. Stronger, still not a test of the vulnerable code.
  3. Behaviourally confirmed. A non-destructive probe whose response differs between fixed and vulnerable code, such as an input the fix rejects. This tests the code, not the label, but only exists for vulnerabilities that have such a difference.
  4. Validated with evidence. The vulnerability's effect demonstrated under explicit authorisation, with the request and response kept.

regreSSHion shows how low the ceiling can be for some CVEs. The bug is a timing race that takes hours of connections at the server's limit to win under lab conditions, so there is no quick, harmless differential probe, and attempting validation means sustained connection load against the target, which is a service impact in its own right. The configuration-level evidence available from outside is thin too. The sshd_config manual says the server "disconnects after this time if the user has not successfully logged in", so in principle one idle connection held past 120 seconds shows whether a timeout is in force. A disconnect is consistent with a nonzero LoginGraceTime, but a firewall or proxy idle timeout produces the same result. No disconnect is consistent with LoginGraceTime 0, or a longer value. For this CVE an outside check can reach config-inferred at best, through the revision, and confirmation belongs on the host.

Evidence to keep with a version-based finding

A finding with these attached can be checked by the owner without re-running the scan. The evidence post argues the general case.

Why active confirmation needs explicit scope

Behavioural confirmation and validation send traffic designed to exercise a vulnerability. That needs authorisation from whoever operates the target, and the operator of the address is not always the organisation whose name points at it. A CDN edge, a SaaS platform behind a CNAME, a shared hosting address or a cloud provider's load balancer can appear in your discovered surface while belonging to someone else, and probing it tests their infrastructure, not yours. The attribution post explains how those hosts enter an inventory. A passive banner read is one connection; an exploit-shaped probe is a different act, and the scope boundary should say which hosts may receive which.

A triage procedure for a version-based finding

  1. Get the raw evidence. The exact bytes, address, port, host name, time and source. If the scanner cannot show them, the finding cannot be triaged, only believed or dismissed.
  2. Establish who answered. Resolve the name, follow CNAMEs, check whether the address belongs to a CDN, load balancer or WAF, compare the certificate and TLS behaviour with the origin's, and look for Via. For SSH behind a balancer, probe more than once and compare banners.
  3. Separate observation from inference. Write down the bytes, the product and version the tool derived, and the CPE it matched. A dispute can end here, when the derived version is not what the bytes say.
  4. Identify the distribution and release. From the banner revision, the OS token in a Server header, the o/ field of a signature, or the host's other services. Record it as an inference.
  5. Check the distribution's tracker, not only NVD. Compare the full package version with the fixed version using that distribution's ordering rules.
  6. If no revision is visible, confirm on the host: query the package manager, and check whether the service restarted after the package was updated. For library CVEs, check which processes still map the old library.
  7. Check preconditions. Platform and C library, enabled modules and features, configuration settings, and whether the vulnerable interface is reachable at all.
  8. Record the outcome with its grade and reason: affected, not affected (with the source that says so), fixed by a backport (with the advisory ID), mitigated by configuration, or unknown pending host confirmation. Then prioritise affected findings by exploitation evidence and exposure, as in the CVSS, EPSS and KEV post.
  9. Ask whether the service needs to be exposed. An SSH daemon reachable only from a management network makes the next version dispute irrelevant.

Questions to ask any external scanner, ours included

  1. Can I see the raw response that produced this version, with the probe, time, address and source?
  2. Which signature or rule turned it into a product and version, and what CPE did it produce?
  3. Do you read distribution revisions from banners and compare them with distribution trackers, or only with upstream ranges?
  4. Which version ordering do you use for Debian and RPM revisions?
  5. What do you report when the revision or version is missing: vulnerable, unknown, or nothing?
  6. How do you tell an origin from a CDN, load balancer, WAF or TLS terminator, and is that shown on the finding?
  7. What grade is each finding: banner-inferred, config-inferred, behaviourally confirmed or validated? Is it labelled?
  8. For confirmed or validated findings, what was sent, from where, when, and under which scope?
  9. Do active checks stay off hosts operated by third parties that appear in my surface?
  10. Which advisory and tracker data, retrieved when, decided the match?
  11. When I mark a finding as fixed by a backport, does it stay closed while the evidence is unchanged, and reopen if the banner changes?

Where Maphra fits

Maphra fingerprints live services as part of discovery; its feature list includes service and technology fingerprinting. Every discovered asset is fingerprinted and checked for exposure, including service versions, certificate and TLS posture and security headers, and each finding carries the raw response that produced it. Its Threat Intelligence Hub correlates advisories, CVE feeds and exploit intelligence against the assets you actually operate, so a CVE only raises an alarm when it touches your surface. Active checks validate exploitability against the live asset within an explicit scope boundary, and the scope gate prevents active checks from reaching third-party hosts that merely appear in your surface. Findings are deduplicated across scans and tracked through new, reopened and fixed states.

That is the whole claim. The fingerprinting, backport, CPE and grading methods in this post are general practice, not a description of how Maphra's checks are implemented. This post does not claim which signatures Maphra uses, whether it parses distribution revisions or consults distribution trackers, which version ordering it applies, how it labels the confidence of a version-based finding, or which CVEs its active checks can validate. Put the questions above to us as you would to anyone else.

Back to ssh.example.com. If the scanner can show the bytes, and they end in "Debian-2+deb12u3" or later by dpkg ordering, the finding is a backport false positive, and the evidence closes it. If the bytes end at "OpenSSH_9.2p1", the honest answer is unknown, and one package query on the host, plus a check that sshd has restarted since the update, settles it.

More about Maphra · All posts