Home › The Stash

Vulnerability scanning lab: find, rank and fix with Greenbone (OpenVAS)

LabNovember 5, 2026

IntermediatePartly testedTime ~2 hours (mostly waiting)Tool Greenbone / OpenVASNeeds Lab 00 + DockerCost freeExam Sec+ 4 · also 2

0 / 12 steps

The goal

Run a real vulnerability scanner, Greenbone Community Edition (the free home of the OpenVAS scanner), against a deliberately old web server that you build yourself. You will read what the scanner found, look the worst findings up in three public sources, decide what to fix first and why, apply the fix, and then scan again to prove the fix worked. That find, rank, fix, verify loop is what vulnerability management means in a real job.

Scan only what you own. The target in this lab is a container you create on a private Docker network that has no route to the internet, inside your own machine. Pointing a vulnerability scanner at someone else’s computer, website or network without their written permission is illegal in most countries, even “just to look”, and scans are noisy enough that people notice. Step 12 removes everything.

Why this lab. A scanner will happily hand you a report with forty “critical” findings. Nobody can fix forty things on Monday morning, so the real skill is deciding which three to fix first, and then proving they are fixed. Security+ asks about exactly this: CVSS scores, why a high score alone isn’t enough, false positives, compensating controls and rescanning to validate a fix.

A scanner is a smoke detector with opinions. It will scream about the toast. Your job is to find the actual fire. — Nyx

Before you start. Use an Ubuntu machine you control: the VM from Lab 00 or Ubuntu 24.04 in WSL2. You need Docker Engine with the Compose plugin (the Docker security lab and Docker’s own install guide cover it), plus sudo apt install curl jq python3. Greenbone is big: allow about 50 GB of disk and 4 GB of free memory. Open a root shell with sudo -i (prompt #) and keep that same terminal open for the whole lab, because later steps reuse things you set earlier.

Words you’ll meet

Term What it means
Vulnerability A weakness in software or settings that an attacker could use. A scanner looks for known ones.
CVE Common Vulnerabilities and Exposures: the public ID a known flaw gets, like CVE-2021-41773. One ID, one flaw, so everyone talks about the same thing.
CVSS Common Vulnerability Scoring System: a 0 to 10 score for how bad a flaw is in general (9.0 and up is “critical”). The base score knows nothing about your network.
EPSS Exploit Prediction Scoring System, run by FIRST: the estimated chance (0 to 1) that a CVE gets exploited somewhere in the next 30 days.
KEV CISA’s Known Exploited Vulnerabilities catalog: CVEs that attackers are confirmed to be using. US federal agencies must fix these by a deadline.
NVT Network Vulnerability Test: one of Greenbone’s individual checks. The feed holds well over a hundred thousand of them.
QoD Quality of Detection: how sure Greenbone is that a finding is real, 0 to 100%. A check that only reads a version number scores lower than one that actually proves the flaw.
False positive The scanner says “vulnerable” but you aren’t. A false negative is the scarier opposite: a real flaw the scanner missed.

Steps

01Download Greenbone and pull its images

Greenbone publishes a Docker Compose file: one text file that lists every container the scanner needs (about twenty, from the database to the web interface) and how they connect. Make a folder for it and download the current version straight from Greenbone’s documentation site.

# mkdir -p ~/greenbone && cd ~/greenbone
# curl -fsSL https://greenbone.github.io/docs/latest/_static/compose.yaml -o compose.yaml
# docker compose -p greenbone pull -q
# docker image ls --format "{{.Repository}}:{{.Tag}} {{.Size}}" | sort

The pull is the slow part: in our run it took 8 minutes and the images came to roughly 12.5 GB. The listing (trimmed) shows the two biggest are the feeds themselves:

...
registry.community.greenbone.net/community/gvmd:stable 1.09GB
...
registry.community.greenbone.net/community/ospd-openvas:stable 1.18GB
...
registry.community.greenbone.net/community/scap-data:latest 3.39GB
registry.community.greenbone.net/community/vulnerability-tests:latest 3.03GB

What is a feed? A scanner is only as good as its list of known problems. Greenbone’s free Community Feed ships the NVTs (the individual tests), SCAP data (the official CVE and product lists from the US National Vulnerability Database) and CERT advisories. They arrive as data containers so the rest of the stack can load them.

02Start Greenbone and set the admin password

Start everything in the background, then replace the default admin password with one of your own. Put your own password in the GVM_PW= line; later steps reuse it.

# docker compose -p greenbone up -d
# GVM_PW='pick-your-own-long-password'
# docker compose -p greenbone exec -u gvmd gvmd gvmd --user=admin --new-password="$GVM_PW"

The password change printed Modifying user password. In our run, up -d took under 3 minutes.

Now make a short helper, gmp, so you can talk to the scanner from the terminal. GMP (Greenbone Management Protocol) is the XML language the web interface itself uses behind the scenes; gvm-cli sends it for you.

# gmp() { docker compose -p greenbone run --rm -T --no-deps gvm-tools gvm-cli --gmp-username admin --gmp-password "$GVM_PW" socket --pretty --xml "$1"; }
# gmp "<get_version/>"
<get_version_response status="200" status_text="OK">
  <version>22.7</version>
</get_version_response>

Prefer clicking? The same scanner has a web interface at https://127.0.0.1 (log in as admin with your password; accept the self-signed certificate warning). This lab sticks to the terminal so every step is copy-and-paste and you can see exactly what happens. Each gmp call starts a small helper container, so expect 5 to 30 seconds per call.

03Wait for the feeds to load

The containers are running, but the manager still has to read every CVE and test into its database. Check on it:

# gmp "<get_feeds/>" | grep -E "<name>|<currently_syncing>"
# gmp "<get_configs/>" | grep -A12 "<name>Full and fast</name>" | grep nvt_count

While a feed is still loading you will see <currently_syncing> under it, and the second command prints nothing. When it’s ready the second command prints a count. Ours, once loaded:

    <name>Greenbone Community Feed</name>
    <name>Greenbone SCAP Data Feed</name>
    <name>Greenbone CERT Data Feed</name>
    <name>Greenbone Data Objects Feed</name>
    <nvt_count>191859<growing>1</growing></nvt_count>

How long? On our machine the full load needed more than an hour and a half after up -d: over an hour importing years of CVE records, then 10 to 15 minutes for the tests themselves. Don’t restart the machine or WSL while it runs; ours was interrupted, which caused both problems in the step 6 note. Go and do something else, then rerun the two commands every ten minutes. Scans started early simply wait in a queue. Full and fast is Greenbone’s standard scan configuration: every safe test, skipping ones that could crash the target.

04Build an old web server to scan

Your target is Apache HTTP Server 2.4.49, a real release from September 2021 that had serious, widely attacked bugs. It runs in a container on vulnlab, a Docker network created with --internal, which means nothing on it can reach the internet. Then plug the scanner into the same network.

# docker network create --internal --subnet 172.30.99.0/24 vulnlab
# docker run -d --name oldweb --network vulnlab --ip 172.30.99.10 httpd:2.4.49
# docker network connect vulnlab greenbone-ospd-openvas-1
# docker exec oldweb httpd -v
# docker run --rm --network vulnlab curlimages/curl:8.11.1 -sI http://172.30.99.10/
# docker run --rm --network vulnlab curlimages/curl:8.11.1 -s -m 5 https://example.com -o /dev/null; echo "internet from vulnlab? exit $?"
...
Server version: Apache/2.4.49 (Unix)
Server built:   Sep 28 2021 07:46:54
HTTP/1.1 200 OK
Date: Thu, 05 Nov 2026 02:02:07 GMT
Server: Apache/2.4.49 (Unix)
...
internet from vulnlab? exit 6

Reading it. The Server: header tells anyone who asks exactly which version is running, which is how an uncredentialed scan (one that has no login on the machine) recognizes it. The last line failed with exit 6, curl’s “couldn’t resolve host”: the network really is cut off from the internet, so this deliberately weak server can’t be reached from outside your machine. greenbone-ospd-openvas-1 is the container that actually sends the tests.

05Point the scanner at it and start the scan

A scan in Greenbone has three parts: a target (what to scan), a task (how to scan it) and the scanner that does the work. Create the target with the TCP ports to try, create the task with the Full and fast configuration, then start it. Each command saves the ID it gets back (target, task, and the report the scan will fill in) so later commands can use it.

# TARGET=$(gmp "<create_target><name>oldweb</name><hosts>172.30.99.10</hosts><alive_tests>Consider Alive</alive_tests><port_list id=\"33d0cd82-57c6-11e1-8ed1-406186ea4fc5\"/></create_target>" | grep -o 'id="[^"]*"' | head -1 | cut -d'"' -f2)
# TASK=$(gmp "<create_task><name>oldweb scan</name><config id=\"daba56c8-73ec-11df-a475-002264764cea\"/><target id=\"$TARGET\"/><scanner id=\"08b69003-5fc2-4037-a479-93b440211c73\"/></create_task>" | grep -o 'id="[^"]*"' | head -1 | cut -d'"' -f2)
# REPORT=$(gmp "<start_task task_id=\"$TASK\"/>" | grep -o '<report_id>[^<]*' | cut -d'>' -f2)
# echo "target $TARGET  task $TASK  report $REPORT"
target 645fac7a-2b87-469a-8549-6c683848447b  task 4cc7c4d4-8311-41bf-9f44-773633bcf947  report f29c6fc0-cb8c-474a-bbd1-27978e620736

The long IDs are built into every Greenbone install: 33d0cd82-… is the port list “All IANA assigned TCP”, daba56c8-… is the “Full and fast” scan configuration and 08b69003-… is the built-in OpenVAS scanner. Consider Alive skips the “is anybody there?” ping, because you already know your target is up.

06Watch it run, then list what it found

Check the progress every few minutes:

# gmp "<get_tasks task_id=\"$TASK\"/>" | grep -E "<status>|<progress>" | head -2

Ours went from Requested to Running to Done in about 6 minutes. Greenbone’s raw results are long XML, so save this tiny Python helper that turns them into one line per finding, worst first:

# cat > ~/greenbone/findings.py <<'EOF'
import sys, xml.etree.ElementTree as ET
# Greenbone XML in, one line per finding out (worst first).
# Add a word, e.g. "python3 findings.py Traversal", for full details of matching findings.
word = sys.argv[1].lower() if len(sys.argv) > 1 else None
rows = []
for r in ET.fromstring(sys.stdin.read()).iter("result"):
    nvt = r.find("nvt")
    if nvt is None:
        continue
    tags = dict(t.split("=", 1) for t in (nvt.findtext("tags") or "").split("|") if "=" in t)
    rows.append({"sev": float(r.findtext("severity") or 0), "qod": r.findtext("qod/value"),
                 "port": r.findtext("port"), "name": r.findtext("name"),
                 "cves": [x.get("id") for x in nvt.iter("ref") if x.get("type") == "cve"],
                 "vector": tags.get("cvss_base_vector", ""), "how": tags.get("vuldetect", ""),
                 "fix": nvt.findtext("solution") or "", "found": (r.findtext("description") or "").strip()})
rows.sort(key=lambda x: -x["sev"])
if word is None:
    print(f"{len(rows)} findings")
for f in rows:
    if word is None:
        more = f" (+{len(f['cves']) - 3} more)" if len(f["cves"]) > 3 else ""
        print(f"{f['sev']:4.1f}  QoD {f['qod']}%  {f['port']:8}  {f['name'][:70]}")
        if f["cves"]:
            print(f"      CVEs: {', '.join(f['cves'][:3])}{more}")
    elif word in f["name"].lower():
        print(f["name"])
        print(f"  severity {f['sev']}  {f['vector']}")
        print(f"  QoD {f['qod']}%   CVEs: {', '.join(f['cves']) or 'none listed'}")
        print("  what it saw: " + " / ".join(" ".join(l.split()) for l in f["found"].splitlines() if l.strip()))
        print(f"  how it checked: {' '.join(f['how'].split())}")
        print(f"  fix: {f['fix']}\n")
EOF
# gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=70 rows=100\"/>" | python3 ~/greenbone/findings.py
3 findings
 5.8  QoD 99%  80/tcp    HTTP Debugging Methods (TRACE/TRACK) Enabled
 2.6  QoD 80%  general/tcp  TCP Timestamps Information Disclosure
 2.1  QoD 80%  general/icmp  ICMP Timestamp Reply Information Disclosure

Only three findings, and none about Apache? That’s Greenbone’s default view hiding things on purpose: min_qod=70 shows only findings it is at least 70% sure of. The TRACE finding is proven (QoD 99%: the scanner sent a TRACE request and the server echoed it straight back). The two “timestamp” items are minor information leaks. Hold that thought for step 7.

If your list has no 80/tcp line at all, the scanner never found the web server. On our first try that happened because the feed load had been interrupted, which left the “Full and fast” configuration without its port scanner. Rebuild Greenbone’s built-in configurations, then start the task again with the REPORT=$(gmp … start_task …) line from step 5. If findings show no CVEs: lines, the CVE database is missing; rebuild it (that one takes well over an hour; ours was still in its final stage after 110 minutes):

# docker compose -p greenbone exec -u gvmd gvmd gvmd --rebuild-gvmd-data=all
# docker compose -p greenbone exec -T -u gvmd gvmd gvmd --rebuild-scap

07Lower the bar, then read one finding properly

Ask for every finding however unsure the scanner is (min_qod=0), then add a word to the helper to zoom in on one of them.

# gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py
# gmp "<get_reports report_id=\"$REPORT\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py Traversal

Now there are 24 findings. Ours, trimmed:

24 findings
 9.8  QoD 30%  80/tcp    Apache HTTP Server 2.4.0 - 2.4.55 HTTP Request Smuggling Vulnerability
 9.8  QoD 30%  80/tcp    Apache HTTP Server 2.4.49 - 2.4.50 Directory Traversal / RCE Vulnerabi
 9.8  QoD 30%  80/tcp    Apache HTTP Server 2.4.49 Multiple Vulnerabilities - Linux
 9.8  QoD 30%  80/tcp    Apache HTTP Server < 2.4.54 Multiple Vulnerabilities - Linux
 9.8  QoD 30%  80/tcp    Apache HTTP Server < 2.4.60 Multiple Vulnerabilities - Linux
 9.8  QoD 30%  80/tcp    Apache HTTP Server <= 2.4.51 Buffer Overflow Vulnerability - Linux
 9.8  QoD 30%  80/tcp    Apache HTTP Server <= 2.4.52 Multiple Vulnerabilities - Linux
 9.0  QoD 30%  80/tcp    Apache HTTP Server < 2.4.55 Multiple Vulnerabilities - Linux
 8.2  QoD 30%  80/tcp    Apache HTTP Server 2.4.7 - 2.4.51 Multiple Vulnerabilities - Linux
...
 5.8  QoD 99%  80/tcp    HTTP Debugging Methods (TRACE/TRACK) Enabled
...
 2.6  QoD 80%  general/tcp  TCP Timestamps Information Disclosure
 2.1  QoD 80%  general/icmp  ICMP Timestamp Reply Information Disclosure

And the detail view of the one we’ll follow through the rest of the lab:

Apache HTTP Server 2.4.49 - 2.4.50 Directory Traversal / RCE Vulnerability - Linux
  severity 9.8  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  QoD 30%   CVEs: none listed
  what it saw: Installed version: 2.4.49 / Fixed version: 2.4.51 / Installation / path / port: 80/tcp
  how it checked: Checks if a vulnerable version is present on the target host.
  fix: Update to version 2.4.51 or later.

Why “CVEs: none listed”? Greenbone links each test to its CVE IDs through its CVE database, and ours was still rebuilding after the interrupted feed load (see the note in step 6), so our run printed none. The finding still gives you what you need: the affected versions and the fixed version, 2.4.51. Apache’s own security page for 2.4 lists every CVE by the version that fixed it; for this finding that’s CVE-2021-42013, the incomplete fix for CVE-2021-41773.

Why were 21 Apache findings hidden? Look at QoD 30% and how it checked: Greenbone read the version from the Server: header you saw in step 4 and compared it with the versions known to be affected. It rates that only 30% sure because Linux distributions such as Ubuntu and Red Hat often fix (“backport”) a security bug without changing the version number, so on a distro-packaged server a version-only match is often a false positive. Hiding them by default means fewer false alarms, at the risk of false negatives, which is exactly what happened in step 6.

So are these real? Confirm before you act. In step 4, httpd -v inside the container also said 2.4.49, and this is Apache’s own build, not a patched distro package. So these are true positives. Real still doesn’t always mean exploitable: Apache’s default setting, Require all denied on the whole filesystem, blocks the classic version of the path traversal attack on this exact setup. That’s luck, not a fix: one changed line in the config and the door opens.

Credentialed vs uncredentialed. Your scan was uncredentialed: it looked at the server from the network, like an outsider, and mostly had to trust what the server said about itself. A credentialed scan is given a login (for example SSH on Linux or an admin account on Windows), so it can read the real installed package list and settings. It finds far more, with far fewer false positives, and it is what most organizations run on their own servers. Agent vs agentless is the same question from another angle: an agent is a small program installed on each machine that checks itself and reports in, which suits laptops that are rarely on the office network; agentless scanning, like this lab, reaches out over the network and installs nothing.

08Look it up: CVSS, EPSS and the KEV catalog

Take six CVEs behind these findings (from Apache’s security page, matched by the fixed versions the scan reported) and ask three free public sources about them: the US National Vulnerability Database (NVD) for the CVSS base score, FIRST’s EPSS service for the chance of exploitation, and CISA’s KEV catalog for proof that attackers are already using it. The sleep 7 keeps you under NVD’s limit for visitors without an API key.

# CVES="CVE-2021-41773,CVE-2021-42013,CVE-2021-44790,CVE-2022-31813,CVE-2023-25690,CVE-2024-38475"
# for c in ${CVES//,/ }; do curl -s "https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=$c" | jq -r --arg c "$c" '.vulnerabilities[0].cve.metrics.cvssMetricV31[0].cvssData | "\($c)  CVSS \(.baseScore) \(.baseSeverity)  \(.vectorString)"'; sleep 7; done
# curl -s "https://api.first.org/data/v1/epss?cve=$CVES" | jq -r '.data[] | "\(.cve)  EPSS \(.epss)  percentile \(.percentile)"'
# curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json -o kev.json
# jq -r '"KEV catalog \(.catalogVersion): \(.count) entries"' kev.json
# jq -r --arg c "$CVES" '.vulnerabilities[] | select(.cveID as $id | $c | split(",") | index($id)) | "\(.cveID)  in KEV since \(.dateAdded)  ransomware: \(.knownRansomwareCampaignUse)"' kev.json
CVE-2021-41773  CVSS 9.8 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2021-42013  CVSS 9.8 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2021-44790  CVSS 9.8 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2022-31813  CVSS 9.8 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2023-25690  CVSS 9.8 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CVE-2024-38475  CVSS 9.1 CRITICAL  CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
CVE-2024-38475  EPSS 0.999570000  percentile 0.999750000
CVE-2023-25690  EPSS 0.845080000  percentile 0.997020000
CVE-2022-31813  EPSS 0.035430000  percentile 0.893300000
CVE-2021-44790  EPSS 0.968400000  percentile 0.998870000
CVE-2021-42013  EPSS 0.999640000  percentile 0.999760000
CVE-2021-41773  EPSS 0.999920000  percentile 0.999860000
KEV catalog 2026.11.04: 1768 entries
CVE-2024-38475  in KEV since 2025-05-01  ransomware: Unknown
CVE-2021-42013  in KEV since 2021-11-03  ransomware: Known
CVE-2021-41773  in KEV since 2021-11-03  ransomware: Known

Reading it. CVSS calls every one of them critical (9.1 or 9.8), so CVSS alone can’t tell you where to start. The vector AV:N/AC:L/PR:N/UI:N means: reachable over the network, easy, no login needed, no user has to click anything; C:H/I:H/A:H means high damage to confidentiality, integrity and availability. EPSS pulls them apart: 0.99992 for CVE-2021-41773 but 0.035 (3.5%) for CVE-2022-31813. A percentile of 0.9999 means the score is higher than 99.99% of all CVEs. KEV settles it: three are confirmed exploited in real attacks, and two of those are known to be used by ransomware gangs.

Prefer a browser? The same facts are on the NVD page for CVE-2021-41773, CISA’s KEV catalog search and FIRST’s EPSS site. EPSS scores are recalculated every day, so yours will be slightly different.

09Decide what to fix first

Scores describe the flaw. Your decision also needs context: is the risky feature even switched on? Ask the old image which of the relevant Apache modules it loads by default:

# docker run --rm httpd:2.4.49 grep -E "^#?LoadModule (lua|proxy|proxy_http|rewrite|cgid)_module" /usr/local/apache2/conf/httpd.conf
#LoadModule lua_module modules/mod_lua.so
#LoadModule proxy_module modules/mod_proxy.so
#LoadModule proxy_http_module modules/mod_proxy_http.so
#LoadModule rewrite_module modules/mod_rewrite.so

The # at the start of each line means the module is switched off. Put it all together:

Order Finding CVSS EPSS KEV Why it sits here
1 CVE-2021-41773 and CVE-2021-42013: path traversal (reading files outside the website), code execution if CGI is on. Scan: “2.4.49 Multiple” and “2.4.49 - 2.4.50 Directory Traversal” 9.8 0.9999 Yes, ransomware Exploited for years, in Apache’s core (no extra module needed), and our exact version
2 CVE-2024-38475: mod_rewrite mapping URLs to files it shouldn’t. Scan: “< 2.4.60 Multiple” 9.1 0.9996 Yes Exploited, but mod_rewrite is switched off here, so less urgent on this box
3 CVE-2021-44790: buffer overflow in mod_lua. Scan: “<= 2.4.51 Buffer Overflow” 9.8 0.968 No Very likely to be exploited somewhere, but mod_lua is off by default
4 CVE-2023-25690: request smuggling through mod_proxy and mod_rewrite. Scan: “2.4.0 - 2.4.55 HTTP Request Smuggling” 9.8 0.845 No Needs two modules that are both off here
5 CVE-2022-31813: mod_proxy can drop a header some apps use for IP-based access rules. Scan: “< 2.4.54 Multiple” 9.8 0.035 No Same “critical” CVSS as number 1, yet the lowest chance of exploitation, the module is off, and Apache itself rates it “low”
Later TRACE enabled; TCP and ICMP timestamps 5.8, 2.6, 2.1 – – Settings, not version bugs; low risk; fix or accept in the next window

How the order was decided. KEV first: someone is already exploiting those, so assume your turn will come. Then high EPSS, then the rest by CVSS. Context moves things within that: asset value (a shop taking card payments outranks a test box), exposure (facing the internet or only reachable inside), and compensating controls that are already in place, such as a module being switched off or a firewall rule in front. Compensating controls buy time and belong in your risk register; they don’t close the finding.

The twist. Here one upgrade fixes all 21 Apache findings at once, so the real answer is simply “upgrade today”. Ranking earns its keep when you have hundreds of machines, each needing a different fix, and only one maintenance window this week.

CVSS told me everything was critical. EPSS told me which critical. KEV told me which critical was already on fire. — Nyx

10Fix it: upgrade the server

The real fix for an outdated server is to update it. Here that means throwing the old container away and starting the current Apache 2.4 release in its place, with the same name and address so the scan target still points at it.

# docker rm -f oldweb
# docker run -d --name oldweb --network vulnlab --ip 172.30.99.10 httpd:2.4
# docker exec oldweb httpd -v
oldweb
c44825e403497be8016d0e71296f9bafa6cbf1a8337f2364807ecaeadc67085c
Server version: Apache/2.4.69 (Unix)
Server built:   Oct 28 2026 01:15:47

On a normal server the same step is your package manager or Windows Update, tested on one machine first and done in an agreed maintenance window. If you can’t patch yet, a compensating control lowers the risk in the meantime: block the port at the firewall, put the site behind a web application firewall, or switch the service off if nobody needs it.

11Rescan to prove the fix

A fix you haven’t verified is a guess. Run the same task again. Greenbone keeps the first report and starts a new one, so save the new report’s ID, wait for Done as in step 6, then list everything in the new report, however unsure.

# REPORT2=$(gmp "<start_task task_id=\"$TASK\"/>" | grep -o '<report_id>[^<]*' | cut -d'>' -f2)
# gmp "<get_tasks task_id=\"$TASK\"/>" | grep -E "<status>|<progress>" | head -2
# gmp "<get_reports report_id=\"$REPORT2\" details=\"1\" filter=\"severity>0 min_qod=0 rows=100\"/>" | python3 ~/greenbone/findings.py

The rescan took about 7 minutes. Even with the bar at zero, every Apache finding is gone:

3 findings
 5.8  QoD 99%  80/tcp    HTTP Debugging Methods (TRACE/TRACK) Enabled
 2.6  QoD 80%  general/tcp  TCP Timestamps Information Disclosure
 2.1  QoD 80%  general/icmp  ICMP Timestamp Reply Information Disclosure

What’s left, and why. The upgrade fixed the version problems. The three survivors are about settings and behavior, which a new version doesn’t change: Apache still answers the old TRACE debugging method (turn it off with the line TraceEnable off in its config), and the system answers timestamp requests that leak a little information about it. That’s normal. Fix what is cheap, and formally accept the rest with a reason and a review date. The two reports side by side are your evidence for the change ticket.

12Clean up

Remove the target and its network, then stop Greenbone. The last command also deletes Greenbone’s stored data (feeds, database and reports); leave it out if you want to scan again later without the long feed load.

# docker rm -f oldweb
# docker network disconnect vulnlab greenbone-ospd-openvas-1
# docker network rm vulnlab
# docker compose -p greenbone down
# docker compose -p greenbone down -v

Not tested as written. On our test machine the Linux system was restarted from outside just before this step, which stopped every container, so we cleaned up our copy directly instead. These five lines are the standard Docker clean-up for what this lab created; if one reports that something is already gone, that is fine. The downloaded images (the list from step 1) still take about 12 GB after this; Docker’s own documentation covers removing images you no longer need.

What you learned

  • find, rank, fix, verify — vulnerability management is a loop, and the rescan is what closes it
  • CVSS — how bad a flaw is in general; a 9.8 tells you severity, not urgency
  • EPSS and KEV — how likely exploitation is, and proof that it is already happening; KEV jumps the queue
  • context — asset value, exposure and compensating controls change the order, but they buy time rather than replace the fix
  • QoD and false positives — version-based findings need confirming; credentialed scans and agents see more than a banner

Tried it? Tell us

Nova built this lab from Steven’s ideas, and we ran steps 1 to 11 before publishing (the clean-up step is marked as untested). If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.

NextOpenSCAP: check a system against a security baseline→

Go deeper

Original walkthrough. Steps 1 to 11 were run on Ubuntu 24.04 (WSL2, kernel 6.6) with Docker Engine 29.9.3 and Compose 2.40.3, using Greenbone Community Edition containers from Greenbone’s own compose file (gvmd 26.43, OpenVAS scanner 23.51, ospd-openvas 22.10, feed of 4 Nov 2026) on 4–5 Nov 2026; outputs are from that run, trimmed where marked with .... Step 12’s clean-up lines were not run as written (see the note there), and our CVE database never finished rebuilding, so our findings show no CVE lines. CVSS, EPSS and KEV results were looked up live on 4 Nov 2026; EPSS changes daily. If something doesn’t match what your machine does, tell me and I will fix it.