Vulnerabilities & Exploits

What is vulnerability management?

HuntRule Team · · 11 min read

A long corridor of identical numbered doors in low light, most shut, three standing ajar, one with a paper tag tied to its handle
On this page

About 6% of published CVEs are ever exploited in the wild. A scanner reports the other 94% with the same red badge.

That gap is the entire job. Buying a scanner takes an afternoon. Running a vulnerability management programme means keeping an asset list honest, ranking findings against evidence instead of severity, attaching a human name to every finding, and proving the fix landed. The scanner produces one input in the middle of that chain.

This post walks the lifecycle and names what breaks at each stage. It ends with a Sigma rule, because the hosts you cannot patch still have to be watched.

Asset inventory

You cannot scan what you do not know you own. Every programme that looks broken at the reporting layer is usually broken here.

The failure mode is not a missing spreadsheet. It is scope drift: a scan policy targeting 10.20.0.0/16, written when the estate was one flat network, and a lab subnet stood up in a different VPC two years later. The scanner reports 100% coverage of what it was told to look at.

CISA's BOD 26-04, issued 10 June 2026, replaced BOD 22-01 and made this explicit for federal agencies. It requires continuous identification and tagging of every agency-owned asset reachable from outside the network on a routable IP, with tags carrying organisation, environment (prod or dev), exposure (public or internal) and asset type. That tag set is a reasonable minimum for anyone, because exposure and environment are exactly the fields prioritisation needs two stages later.

Reconcile the scanner's asset list against two independent sources, usually the cloud provider's inventory API and the EDR agent list. Assets present in one and absent from another are the finding. An attacker runs the same enumeration, as T1595.002 from outside and T1046 from inside.

Discovery: two scans, two truths

An unauthenticated scan talks to a port and infers. An authenticated scan logs in and reads the package database. They disagree constantly, and both are right about different things.

Banner inference is where most disputes start. Enterprise Linux distributions backport security fixes without bumping the upstream version string, so a remote check that reads OpenSSH_8.7 and matches it against upstream release notes reports vulnerabilities that were patched months ago. The credentialed check reads the installed package release and sees the fix. That is a scanner false positive, not a patching failure.

The reverse gap matters more. Unauthenticated scans miss client-side software, local service misconfiguration and anything behind an authenticated web path. Authenticated scans miss what an attacker sees on the wire: which of those services is reachable from the internet at all.

Run both. Treat the unauthenticated result as the exposure map and the authenticated result as the truth about installed versions. Where a service is un-scannable, record that, rather than letting silence read as clean.

Prioritisation: five inputs, none sufficient alone

Severity alone produces a list nobody works. A CVSS base score describes technical characteristics of the vulnerability. It was never built to predict attacker behaviour, and Cyentia and FIRST measured that directly: CVSS is a poor predictor of exploitation.

The usable inputs, and how each one misranks on its own:

Input

What it tells you

How it misranks alone

CVSS base score

Intrinsic technical severity, no environment

Ranks thousands of never-exploited criticals above an exploited medium

CISA KEV listing

Exploitation confirmed in the wild

Backward-looking. A CVE lands here after exploitation, not before

EPSS probability

Chance of exploitation activity in the next 30 days

A probability, not a verdict. Low scores still get exploited, rarely

Asset exposure

Internet-reachable, internal, or air-gapped

An exposed printer outranks a domain controller if you stop here

Business context

Data class, blast radius, revenue path

Purely local. No feed supplies it, so it is the input people skip

Read EPSS carefully. The score is a probability between 0 and 1 over a 30-day window, and the percentile is its rank against every other scored CVE. Those move differently: a probability of 0.10 sits near the 95th percentile, so "10% chance" and "top 5% of all CVEs" describe the same vulnerability. EPSS v4, released 17 March 2025, also scores CVEs still in the NVD analysis backlog without a CVSS score.

Combine the inputs as a gate, not a formula. BOD 26-04 does exactly that on four axes: whether CISA has added the CVE to KEV, whether the asset is publicly exposed, whether an adversary can automate every step of the exploit, and whether exploitation yields partial or total control of the asset. The first two are dynamic, so pulling a host off the internet moves it to a longer timeline and a KEV addition shortens it. At the shortest tier, three calendar days, the directive also requires forensic triage of the asset on the assumption it may already be compromised. That last part is the detection engineer's cue.

Remediation ownership

A finding assigned to a queue never closes. A finding assigned to a person closes or gets escalated, and both outcomes are progress.

The ticket needs four fields the scanner does not supply: the named owner of the service (not the team), the change window the fix can land in, the reboot or restart requirement, and the rollback plan. Tooling that opens tickets automatically tends to fill the first field with a group alias, which is functionally the same as leaving it blank.

Ownership disputes are the real backlog. The classic shape is a container image built by one team, deployed by another, running a base OS package owned by nobody. Fix that at the inventory stage with a required owner attribute on the asset record, or it resurfaces on every scan.

Exceptions that expire

NIST SP 800-40 Rev. 4 (April 2022) lists accepting the risk as one of four legitimate responses to a vulnerability, alongside mitigating it, transferring it and avoiding it by eliminating the attack surface. Accepting risk is not a failure. Accepting it silently and forever is. An exception granted once becomes permanent through a specific mechanism: nothing on the record forces a second decision.

A faded paper lockout tag hanging from an industrial valve handle by a corroded wire seal

This is the minimum shape we use for an exception record:

exception_id: VE-2026-0184
cve: CVE-2023-46604
asset: mq-prod-03.internal
service_owner: name of a person, not a distribution list
justification: broker upgrade blocked by vendor app certified only on 5.15.x
attempted_remediation: upgrade tested 2026-06-12, rolled back on client auth failure
compensating_controls:
  - openwire 61616 restricted to app subnet at the host firewall
  - detection rule active on broker child process creation
  - weekly review of outbound HTTP from the broker service account
residual_risk: high
approved_by: named risk owner with authority to accept this rating
approved_on: 2026-06-20
expires_on: 2026-09-18
on_expiry: revert to open finding, no auto-renewal, re-approval requires new justification

Three fields do the work. Compensating controls turn the exception into a work item for the detection team. Residual risk forces a rating somebody has to sign. The 90-day expiry with no auto-renewal stops the record outliving the person who wrote it.

Verification: where programmes quietly stop

Most programmes stop after the ticket closes. The ticket closing is a claim. The rescan is the evidence.

Patch installed does not mean patch running. A Linux host with a new glibc still runs the old one in every process started before the update, and a Java broker holds its old jars until the JVM restarts. Check it:

# RHEL family: list services still running pre-update code
dnf needs-restarting -s

# Debian family (debian-goodies)
checkrestart

Two rules keep this honest. Never close a finding on the remediator's word, close it on the next scan result. And rescan on a cadence shorter than your longest exception window, otherwise expired exceptions and unverified fixes pile up in the same blind spot.

Compensating detection for what you cannot patch

Take the exception above at face value. mq-prod-03 runs Apache ActiveMQ 5.15.x, vulnerable to CVE-2023-46604. The OpenWire protocol marshaller deserialises untrusted data, and a remote attacker with network access to the broker can instantiate an arbitrary class on the classpath. Apache shipped 5.15.16, 5.16.7, 5.17.6 and 5.18.3 on disclosure. CISA added it to KEV on 2 November 2023, and public exploitation delivered HelloKitty and TellYouThePass ransomware and Kinsing cryptomining.

The known public exploit abuses org.springframework.context.support.ClassPathXmlApplicationContext, bundled with the broker, whose constructor accepts a String that can be an HTTP URL to a remote XML bean definition. The bean that comes back runs a command. So the broker's java.exe becomes a parent process, and an ActiveMQ broker almost never spawns children.

title: Suspicious child process of an Apache ActiveMQ broker
id: 7c3f1b64-5a2e-4a17-9f0b-2d8c6ae41f39
status: experimental
description: |
    Detects a process spawned by a java.exe instance whose command line references
    ActiveMQ. The broker rarely spawns child processes, so any shell, script host
    or downloader beneath it is worth an alert. Exploitation of CVE-2023-46604 via
    ClassPathXmlApplicationContext lands here.
references:
    - https://nvd.nist.gov/vuln/detail/CVE-2023-46604
    - https://activemq.apache.org/news/cve-2023-46604
author: HuntRule
date: 2026/07/31
tags:
    - attack.initial-access
    - attack.execution
    - attack.t1190
    - attack.t1059.003
    - attack.t1105
logsource:
    category: process_creation
    product: windows
detection:
    selection_parent:
        ParentImage|endswith: '\java.exe'
        ParentCommandLine|contains:
            - 'activemq'
            - 'apache-activemq'
    filter_main_runtime:
        Image|endswith:
            - '\conhost.exe'
            - '\java.exe'
            - '\jstack.exe'
            - '\jcmd.exe'
    condition: selection_parent and not 1 of filter_main_*
falsepositives:
    - Broker start and stop wrappers that shell out to cmd.exe on service control
    - Monitoring agents running diagnostic commands under the broker service account
    - Unrelated Java applications carrying an activemq-client jar on the classpath
level: high

What it catches: any exploitation chain that ends in a new process under the broker. Downloaders (curl.exe, certutil.exe), shells (cmd.exe, powershell.exe) and ransomware binaries all surface as T1190 followed by T1059.003 and T1105.

What it misses matters more than the rule:

  • Payloads that stay inside the JVM. A bean that opens a socket or writes a file without forking never creates a process_creation event.

  • Brokers launched under a service wrapper. If ParentImage is wrapper.exe and java.exe is the grandparent, the rule does not fire. Check how your broker actually starts.

  • Linux brokers, which are the common deployment. The logsource is product: windows. Port the parent-child logic to auditd or your EDR's Linux process telemetry.

  • The attempt itself. Success and failure look identical until a child spawns. Pair this with egress monitoring on the broker service account, since the exploit makes the broker fetch a remote XML file over HTTP.

One telemetry constraint decides whether the rule works at all. Windows Security event ID 4688 records the creator process name but carries no parent command line, so the ParentCommandLine condition cannot evaluate. You need Sysmon event ID 1 or an EDR that supplies it. With 4688 only, drop that clause, scope the rule to known broker hostnames, and accept the wider noise.

We modelled this on the Huntress DE&TH team's public rule for the same CVE. At the time of writing, that rule's condition references a selection named Selection1 while the block above it is named selection, so it will not compile in sigma-cli as published. Compile every rule you copy before you trust it, ours included. That habit is the through-line of detection engineering.

Related rules live in the catalog at /rules?q=activemq.

Summary

Vulnerability management is an inventory problem, a prioritisation problem and an accountability problem that arrives dressed as a scanning problem. Rank on exploitation evidence and exposure rather than CVSS alone. Give every finding a named owner, every exception an expiry date and a compensating control, and close findings on rescan evidence rather than on a claim. For systems that will never be patched, the exception record is where a detection requirement gets written down, not where risk goes to be forgotten.

Telemetry to have in place before you need it:

Windows:
  Sysmon Event ID 1   process creation with ParentImage + ParentCommandLine
  Sysmon Event ID 3   network connection, tagged by service account
  Security 4688       fallback only, no ParentCommandLine field
  Security 4624/4625  logon activity on hosts under active exception

Linux:
  auditd execve       parent PID resolution for service-account children
  EDR process tree    required where auditd parent context is lost

Network:
  egress HTTP/S from server service accounts, by process where available
  inbound connections to 61616/tcp (OpenWire) from outside the app subnet

Asset side:
  exposure tag (public|internal) on every asset record
  named service owner attribute, populated, not a group alias
  exception expiry date, queryable, alerting 14 days before expiry

Start with critical findings on internet-facing assets at /rules?severity=critical and work inward.

Related articles