From ea813b754f7cc596ec45c50fb22c1b80a5e47caa Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 21 Sep 2026 11:11:17 +0000 Subject: [PATCH] Don't confirm Shellshock CRITICAL from header reflection probe_shellshock treated any response containing the injected User-Agent marker as CVE-2014-6271 RCE. Apache printenv and other CGIs that echo request headers produced a fusion-pinned CRITICAL without bash executing the payload. Confirm only the evaluated arithmetic token, which reflection cannot produce. Co-authored-by: dmitryflynn --- src/vuln_prober.py | 18 ++++++++++++++--- test_probers.py | 49 +++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/vuln_prober.py b/src/vuln_prober.py index df0807e..903f72f 100644 --- a/src/vuln_prober.py +++ b/src/vuln_prober.py @@ -287,6 +287,16 @@ def probe_grafana_path_traversal(host: str, port: int, scheme: str = "http", return None +# Confirmation token is the *evaluated* result of bash arithmetic in the +# injected payload, not the payload string itself. Apache printenv and any +# CGI that reflects User-Agent/Referer/Cookie would otherwise confirm +# CVE-2014-6271 CRITICAL (fusion-pinned) without executing bash. +_SHELLSHOCK_FACTORS = (7, 191, 13) # product = 17381 +_SHELLSHOCK_TOKEN = ( + f"NETLOGIC_{_SHELLSHOCK_FACTORS[0] * _SHELLSHOCK_FACTORS[1] * _SHELLSHOCK_FACTORS[2]}" +) + + def probe_shellshock(host: str, port: int, scheme: str = "http", timeout: float = 5.0) -> Optional[CVEProbe]: """CVE-2014-6271 — Shellshock: bash CGI remote code execution via environment injection.""" @@ -295,7 +305,8 @@ def probe_shellshock(host: str, port: int, scheme: str = "http", "/cgi-bin/status", "/cgi-bin/env", "/cgi-bin/index.cgi", "/cgi-bin/bash", "/cgi-bin/run.cgi", ] - payload = "() { :;}; echo; echo; echo NETLOGIC_SHELLSHOCK_CONFIRMED" + a, b, c = _SHELLSHOCK_FACTORS + payload = f"() {{ :;}}; echo; echo; echo NETLOGIC_$(({a}*{b}*{c}))" for path in cgi_paths: req = ( f"GET {path} HTTP/1.1\r\n" @@ -306,7 +317,7 @@ def probe_shellshock(host: str, port: int, scheme: str = "http", f"Connection: close\r\n\r\n" ).encode() resp = _raw_http(host, port, req, timeout=timeout) - if resp and "NETLOGIC_SHELLSHOCK_CONFIRMED" in resp: + if resp and _SHELLSHOCK_TOKEN in resp: return CVEProbe( cve_id="CVE-2014-6271", title="Shellshock — Bash CGI Remote Code Execution", @@ -314,7 +325,8 @@ def probe_shellshock(host: str, port: int, scheme: str = "http", detail=(f"Shellshock confirmed at {path}. " "Bash function definition in HTTP headers executes arbitrary commands. " "Full unauthenticated OS command execution as the web server user."), - evidence=f"GET {path} with Shellshock payload in User-Agent → 'NETLOGIC_SHELLSHOCK_CONFIRMED' in response", + evidence=(f"GET {path} with Shellshock payload in User-Agent → " + f"'{_SHELLSHOCK_TOKEN}' in response"), remediation="Update bash to a patched version (>= 4.3-patch 25); disable CGI scripts; upgrade OS." ) return None diff --git a/test_probers.py b/test_probers.py index eb4287c..371fa86 100644 --- a/test_probers.py +++ b/test_probers.py @@ -310,19 +310,58 @@ def test_shellshock_marker_required_and_safe(monkeypatch): seen = {} def fake(host, port, req, timeout=5.0): seen["req"] = req - return "HTTP/1.1 200 OK\r\n\r\n\n\nNETLOGIC_SHELLSHOCK_CONFIRMED\n" + return f"HTTP/1.1 200 OK\r\n\r\n\n\n{vp._SHELLSHOCK_TOKEN}\n" monkeypatch.setattr(vp, "_raw_http", fake) f = vp.probe_shellshock("h", 80) assert f and f.confirmed - # payload must only echo a marker — no destructive shell commands - req = seen["req"].decode("latin1").lower() + # payload must only echo a computed marker — no destructive shell commands + req = seen["req"].decode("latin1") + req_l = req.lower() for danger in ("rm ", "cat /etc/shadow", "wget", "curl", "/bin/sh -c", "mkfifo", ";reboot"): - assert danger not in req - # Normal CGI output without the marker → no finding. + assert danger not in req_l + # The evaluated token must not appear in the injected headers, otherwise a + # CGI that reflects User-Agent (Apache printenv) would confirm CRITICAL. + assert vp._SHELLSHOCK_TOKEN not in req + # Normal CGI output without the evaluated token → no finding. monkeypatch.setattr(vp, "_raw_http", lambda *a, **k: "HTTP/1.1 200 OK\r\n\r\nhello world") assert vp.probe_shellshock("h", 80) is None +def test_shellshock_header_reflection_is_not_confirmation(monkeypatch): + """Apache printenv / error pages that echo UA must not pin Shellshock RCE.""" + captured = {} + + def reflect(host, port, req, timeout=5.0): + captured["req"] = req + body = req.decode("latin1") + return ( + "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n" + "SERVER_SOFTWARE=Apache/2.4.41\n" + f"HTTP_USER_AGENT={body.split('User-Agent: ', 1)[-1].split(chr(13), 1)[0]}\n" + f"HTTP_REFERER={body.split('Referer: ', 1)[-1].split(chr(13), 1)[0]}\n" + ) + + monkeypatch.setattr(vp, "_raw_http", reflect) + assert vp.probe_shellshock("h", 80) is None + # Sanity: the request really did carry the unevaluated arithmetic payload. + assert b"NETLOGIC_$((" in captured["req"] + + +def test_shellshock_execution_still_confirms_when_env_is_also_dumped(monkeypatch): + """A bash CGI that dumps env after executing still confirms (true positive).""" + def fake(host, port, req, timeout=5.0): + body = req.decode("latin1") + ua = body.split("User-Agent: ", 1)[-1].split("\r", 1)[0] + return ( + "HTTP/1.1 200 OK\r\n\r\n" + f"{vp._SHELLSHOCK_TOKEN}\n" + f"HTTP_USER_AGENT={ua}\n" + ) + monkeypatch.setattr(vp, "_raw_http", fake) + f = vp.probe_shellshock("h", 80) + assert f and f.confirmed and f.cve_id == "CVE-2014-6271" + + def test_spring_actuator_detect_vs_empty(monkeypatch): monkeypatch.setattr(vp, "_http_get", lambda h, p, path, **k: (200, '{"propertySources":[]}') if path == "/actuator/env" else (404, ""))