Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/vuln_prober.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -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"
Expand All @@ -306,15 +317,16 @@ 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",
severity="CRITICAL", confirmed=True,
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
Expand Down
49 changes: 44 additions & 5 deletions test_probers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, ""))
Expand Down
Loading