← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
tenable:173251 · CWE-523 · Disclosed 2023-03-22

Apache Tomcat 9

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

Like a valet tagging your keys as private but forgetting to put them in the locked drawer

This maps to CVE-2023-28708. In Apache Tomcat 9.0.0-M1 through 9.0.71, when RemoteIpFilter is used behind a reverse proxy and requests arrive over HTTP with X-Forwarded-Proto: https, Tomcat can issue session cookies without the Secure attribute. The result is not code execution on the server; it is a session-cookie transport weakness that can let a browser send JSESSIONID over an insecure channel.

The vendor MEDIUM label is fair in a vacuum, but it is a little generous for enterprise patch triage. Real exploitation needs a very specific deployment pattern and a second condition on the victim side: the attacker must either observe or induce an HTTP request path where that cookie is exposed. That compounding friction pushes this down to LOW for most fleets.

"This is a narrow cookie-leak bug behind a proxy, not a server-takeover event."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find the narrow deployment pattern

The attacker first needs a Tomcat application running a vulnerable 9.x build where RemoteIpFilter is actually enabled and the app sits behind a reverse proxy that forwards X-Forwarded-Proto. Typical tooling here is Nessus plugin 173251 for version detection plus Burp Suite or curl to inspect headers and cookie behavior. Version alone is not enough; reachability depends on config.
Conditions required:
  • Apache Tomcat 9.0.0-M1 through 9.0.71
  • RemoteIpFilter enabled in Tomcat or app config
  • Reverse proxy forwards X-Forwarded-Proto
  • Proxy-to-Tomcat traffic reaches Tomcat over HTTP
Where this breaks in practice:
  • Many Tomcat instances do not use RemoteIpFilter at all
  • Some environments terminate TLS differently or set cookie flags at the proxy
  • Internet-facing banners do not prove this exact configuration
Detection/coverage: Scanners like Nessus reliably flag the vulnerable version, but they generally do not prove the required filter/proxy topology; defenders need config review or response inspection.
STEP 02

Confirm insecure cookie issuance

Using Burp Suite, curl, or browser dev tools, the attacker requests the application through the proxy and checks Set-Cookie for JSESSIONID without the Secure flag. This confirms the bug is live in that path. No server memory corruption or exploit payload is involved; this is pure response-side misconfiguration behavior.
Conditions required:
  • Application issues a session cookie on the tested path
  • Attacker can reach the app through the same reverse-proxy route users take
Where this breaks in practice:
  • Some apps or proxies rewrite cookies and add Secure before they hit the browser
  • Stateless apps or paths that do not issue a session cookie give the attacker nothing to steal
Detection/coverage: Easy to validate with synthetic transactions, web tests, and response-header inspection in CI or at the edge.
STEP 03

Create or observe an HTTP leak path

The attacker then needs a way to make the victim browser send that cookie over HTTP, commonly using mitmproxy, bettercap, or a simple downgrade/insecure-link scenario. In practice this means an on-path attacker, an externally reachable HTTP endpoint for the same site, or a user tricked into an insecure request. This is the decisive friction point in the chain.
Conditions required:
  • Victim browser has the affected session cookie
  • There is a reachable HTTP path for the same origin or an on-path adversary can observe/inject traffic
  • User interaction or user browsing activity occurs
Where this breaks in practice:
  • HSTS and HTTPS-only redirects sharply reduce practical exploitation
  • Modern enterprises often block plaintext access at the edge
  • Captive-portal or hostile-Wi-Fi style positions are not universal attacker positions
Detection/coverage: Look for unexpected HTTP requests to authenticated app domains, downgrade attempts, and proxy logs showing plaintext access to login-protected paths.
STEP 04

Replay the session cookie

Once the attacker captures JSESSIONID, they can replay it with Burp Repeater or curl to impersonate the victim session. Impact is typically limited to the victim's web session and whatever that account can do. This is account/session hijack risk, not host takeover.
Conditions required:
  • Captured session cookie remains valid
  • Application does not strongly bind the session to IP, device, or step-up auth
Where this breaks in practice:
  • Short session lifetimes can expire the window
  • MFA at re-auth or privileged action steps can contain blast radius
  • Session binding and anomaly detection can break replay
Detection/coverage: Web-session analytics, impossible-travel/user-agent changes, and duplicate-session alerts can catch replay; endpoint scanners will not.
03 · Intelligence Metadata

The supporting signals.

Primary mappingTenable plugin 173251 maps to CVE-2023-28708 for Tomcat 9.x, with the plugin explicitly calling out Tomcat 9.0.0.M1 < 9.0.72.
In-the-wild statusNo strong public evidence of active exploitation found. CERT Santé marks active exploitation as No, and I found no CISA KEV listing or campaign reporting tied to this CVE.
Public proof-of-conceptNo meaningful public exploit tooling signal. CERT Santé says no open-source proof of concept is available; this fits the bug class because exploitation is mostly header/cookie validation plus traffic interception, not a flashy RCE chain.
EPSSLow exploit prediction. A FIRST-fed snapshot surfaced by Wiz shows roughly 0.10% EPSS and about the 27.7th percentile, which is consistent with a niche, low-weaponization bug.
KEV statusNot in CISA KEV. No KEV due date, no federal emergency patch driver, and no public evidence that CISA considers this one actively exploited at scale.
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N = remote reachability, but only low confidentiality impact, with user interaction required and no integrity/availability effect.
Affected rangeTomcat 9.0.0-M1 through 9.0.71 are affected in the 9.x line. Upstream also lists 8.5.0-8.5.85, 10.1.0-M1-10.1.5, and 11.0.0-M1-M2.
Fixed versions and backportsUpstream fix is 9.0.72. Distro backports exist, including Ubuntu 22.04 9.0.58-1ubuntu0.1+esm4, Ubuntu 20.04 9.0.31-1ubuntu0.8, Debian backports for tomcat9, and SUSE package updates such as tomcat >= 9.0.36-3.102.1 on SLES 12 SP5.
Exposure realityNo reliable internet-wide census for this CVE specifically. Shodan/Censys-style Tomcat counts overstate the population because this bug is not banner-detectable; it only matters when RemoteIpFilter is enabled behind a proxy and the browser still has some HTTP path to the app.
Disclosure and reportingUpstream says the underlying bug was reported publicly on 2023-02-08 via Apache Bugzilla 66471; the Tomcat Security Team identified the security implications on 2023-02-09 and made the issue public on 2023-03-22.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.1/10)

The decisive factor is that this bug needs a specific reverse-proxy + RemoteIpFilter deployment pattern and then still needs a victim-side HTTP leakage path before an attacker gets value. That makes it materially less urgent than the vendor baseline suggests for a 10,000-host patch queue, because the reachable population and the blast radius are both narrow.

HIGH Technical preconditions and impact mechanics
MEDIUM Real-world exposure population across enterprise Tomcat estates

Why this verdict

  • Downgrade from vendor MEDIUM: the vendor baseline assumes a vulnerable version matters on its own, but this CVE is only reachable when RemoteIpFilter is enabled behind a proxy that forwards X-Forwarded-Proto over an HTTP backend leg.
  • Second-stage friction matters: exploitation still needs a browser-side HTTP leak or an on-path attacker. If you already enforce HTTPS-only with HSTS and no plaintext access to the app origin, the practical path collapses fast.
  • Blast radius is session-level, not server-level: the outcome is stolen JSESSIONID and session replay, not RCE, not arbitrary file read, and not a universal auth bypass across the server.

Why not higher?

There is no server compromise here by itself. No CISA KEV listing, no strong public exploitation signal, no public PoC momentum, and the chain depends on both a narrow config pattern and a victim-side transport weakness; that is not how HIGH or CRITICAL patch drivers behave in real fleets.

Why not lower?

It is still a real security defect in a common platform, and plenty of enterprises do run Tomcat behind reverse proxies. If the application still exposes HTTP anywhere on the same origin, a stolen session cookie can become immediate account hijack for authenticated users, so this is not pure noise.

05 · Compensating Control

What to do — in priority order.

  1. Force Secure at the edge — Configure the reverse proxy or load balancer to append the Secure attribute to JSESSIONID (and ideally HttpOnly and SameSite) where application behavior allows it. For a LOW verdict there is no fixed SLA; treat this as backlog hygiene, but do it quickly if the edge rule is low-risk and easy.
  2. Kill plaintext access — Enforce HTTPS-only, redirect port 80 immediately, and enable HSTS on the public origin so the browser never has a practical HTTP path to send the cookie. For LOW, there is no mitigation SLA; this is still the best compensating control because it removes the attacker’s key friction bypass.
  3. Inventory RemoteIpFilter usage — Find Tomcat instances where org.apache.catalina.filters.RemoteIpFilter is configured in conf/ or app web.xml files, and prioritize only those systems for upgrade validation. For LOW, there is no fixed SLA; use this to trim patch scope instead of blindly burning change windows on every Tomcat host.
  4. Hunt for authenticated HTTP traffic — Search proxy and web logs for HTTP requests to domains that should be HTTPS-only, especially authenticated routes or paths that set JSESSIONID. For LOW, there is no fixed SLA; use this as a quick exposure check before maintenance windows.
What doesn't work
  • HttpOnly alone does not fix this; it stops JavaScript reads, not browser transmission over HTTP.
  • Encrypting only the proxy-to-Tomcat backend leg does not solve the browser-side cookie leak if the site is still reachable over HTTP.
  • A generic WAF signature is weak here because the issue is missing cookie metadata and session replay, not a distinctive exploit payload.
06 · Verification

Crowdsourced verification payload.

Run this on the target Tomcat host or from a mounted filesystem copy of the Tomcat install. Invoke it as python3 verify_cve_2023_28708.py /opt/tomcat and use root only if you need to read all app WEB-INF/web.xml files; otherwise the Tomcat service account is usually enough.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
"""
verify_cve_2023_28708.py
Checks whether a Tomcat 9 installation is vulnerable to CVE-2023-28708.

Logic:
- If Tomcat version is 9.0.72 or later => PATCHED
- If Tomcat version is 9.0.0-M1 through 9.0.71 and RemoteIpFilter is found in common config/app files => VULNERABLE
- If version is vulnerable but RemoteIpFilter is not found => UNKNOWN
- If version cannot be determined => UNKNOWN

Exit codes:
0 = PATCHED
1 = VULNERABLE
2 = UNKNOWN
"""

import os
import re
import sys
import zipfile
from pathlib import Path

REMOTE_IP_PATTERNS = [
    "RemoteIpFilter",
    "org.apache.catalina.filters.RemoteIpFilter",
]


def read_text(path):
    try:
        return Path(path).read_text(errors="ignore")
    except Exception:
        return None


def get_version_from_release_notes(base):
    for rel in [base / "RELEASE-NOTES", base / "RUNNING.txt", base / "RELEASE-NOTES.txt"]:
        txt = read_text(rel)
        if not txt:
            continue
        m = re.search(r"Apache Tomcat Version\s+([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", txt)
        if m:
            return m.group(1)
        m = re.search(r"Apache Tomcat/([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", txt)
        if m:
            return m.group(1)
    return None


def get_version_from_manifest(base):
    jar_candidates = [
        base / "lib" / "catalina.jar",
        base / "lib" / "tomcat-catalina.jar",
    ]
    for jar in jar_candidates:
        if not jar.exists():
            continue
        try:
            with zipfile.ZipFile(jar, "r") as zf:
                for name in ["META-INF/MANIFEST.MF", "org/apache/catalina/util/ServerInfo.properties"]:
                    try:
                        data = zf.read(name).decode("utf-8", errors="ignore")
                    except Exception:
                        continue
                    m = re.search(r"(?:Implementation-Version|Server number):\s*([0-9]+\.[0-9]+\.[0-9]+(?:[-.][A-Za-z0-9]+)?)", data)
                    if m:
                        return m.group(1)
        except Exception:
            pass
    return None


def normalize_version(v):
    # Supports 9.0.71 and 9.0.0-M1 style values.
    # milestone/build suffixes sort below final releases.
    if not v:
        return None
    m = re.match(r"^(\d+)\.(\d+)\.(\d+)(?:[-.]?([A-Za-z]+)(\d+)?)?$", v)
    if not m:
        return None
    major, minor, patch = int(m.group(1)), int(m.group(2)), int(m.group(3))
    label = (m.group(4) or "").upper()
    label_num = int(m.group(5) or 0)
    order = {"M": -3, "MILESTONE": -3, "RC": -2, "B": -1, "": 0}
    return (major, minor, patch, order.get(label, 0), label_num)


def version_lt(a, b):
    na, nb = normalize_version(a), normalize_version(b)
    if na is None or nb is None:
        return None
    return na < nb


def version_is_tomcat9(v):
    nv = normalize_version(v)
    return nv is not None and nv[0] == 9


def find_remote_ip_filter(base):
    candidates = []
    conf = base / "conf"
    if conf.exists():
        candidates.extend(conf.rglob("*.xml"))
    webapps = base / "webapps"
    if webapps.exists():
        candidates.extend(webapps.rglob("WEB-INF/web.xml"))

    hits = []
    for path in candidates:
        try:
            txt = path.read_text(errors="ignore")
        except Exception:
            continue
        if any(p in txt for p in REMOTE_IP_PATTERNS):
            hits.append(str(path))
    return hits


def main():
    if len(sys.argv) != 2:
        print("UNKNOWN - usage: python3 verify_cve_2023_28708.py /path/to/tomcat")
        sys.exit(2)

    base = Path(sys.argv[1]).resolve()
    if not base.exists() or not base.is_dir():
        print(f"UNKNOWN - Tomcat base not found: {base}")
        sys.exit(2)

    version = get_version_from_release_notes(base) or get_version_from_manifest(base)
    if not version:
        print("UNKNOWN - could not determine Tomcat version")
        sys.exit(2)

    if not version_is_tomcat9(version):
        print(f"UNKNOWN - detected Tomcat version {version}; this script only assesses the Tomcat 9 plugin scope")
        sys.exit(2)

    cmp_res = version_lt(version, "9.0.72")
    if cmp_res is None:
        print(f"UNKNOWN - could not compare version {version} to 9.0.72")
        sys.exit(2)

    if not cmp_res:
        print(f"PATCHED - Tomcat {version} is not in the vulnerable 9.x range for CVE-2023-28708")
        sys.exit(0)

    hits = find_remote_ip_filter(base)
    if hits:
        print(f"VULNERABLE - Tomcat {version} is < 9.0.72 and RemoteIpFilter was found in: {', '.join(hits[:10])}")
        sys.exit(1)

    print(f"UNKNOWN - Tomcat {version} is < 9.0.72 but RemoteIpFilter was not found in common configs; manual review of proxy/cookie behavior is required")
    sys.exit(2)


if __name__ == "__main__":
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not let this outrank your remotely exploitable Tomcat issues. First, identify the subset of Tomcat 9 systems that actually use RemoteIpFilter behind a reverse proxy and confirm whether the public origin still permits any HTTP path; for a LOW verdict, the noisgate mitigation SLA does not apply and the noisgate remediation SLA has no fixed deadline either, so treat it as backlog hygiene. If you can add Secure at the edge or shut off plaintext access with a low-risk proxy change, do that during normal operations; otherwise roll the upgrade to 9.0.72+ in routine maintenance after higher-value internet-facing flaws are cleared.

Sources

  1. Tenable Nessus Plugin 173251
  2. Apache Tomcat 9 security page
  3. NVD CVE-2023-28708
  4. Tomcat filter configuration reference
  5. Ubuntu CVE-2023-28708
  6. Debian Security Tracker CVE-2023-28708
  7. SUSE CVE-2023-28708
  8. CERT Santé advisory for CVE-2023-28708
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.