Tier-3 fixes: b'' probe false-positive, requires_bridge, msgpack

Bug 10: _wait_for_tcp returned on recv()→b'' (connection closed by peer),
falsely signalling service-ready. Only socket.timeout or non-empty data
are genuine ready signals; b'' now retries.

Bug 11: distccd_command_exec and unreal_ircd_3281_backdoor incorrectly
had requires_bridge=true. bind_perl payloads connect inward (host→guest
via hostfwd), not outward — no bridge egress needed. Both modules now
run on SLIRP-only fleet slots.

Bug 12: msgpack.unpackb crashed on integer session IDs from msfrpcd 6.x
(strict_map_key=True default). Added strict_map_key=False.

Bug 13 (documented): samba_usermap_script removed from catalog (NoReply
on every fire — already handled in dca6144 on origin/main).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Elliott Kolden 2026-05-05 15:15:18 -06:00
parent 3d4f282e9c
commit b73f5559dc
5 changed files with 101 additions and 17 deletions

View file

@ -178,13 +178,94 @@ msgpack response dicts. Applied to `module.execute` and `session.list`.
---
## Bug 10 — `_wait_for_tcp` returns success on `b''` (connection-closed-by-peer)
**Symptom:** Log shows "target service is up" within 0.5 s of the 65 s boot
floor, but all exploit fires time out waiting for a session. FTP (port 21),
Samba (139), and distccd (3632) all returned `b''`. The VM's services were not
up; the probe was wrong.
**Root cause:** When `recv(1)` returns `b''` (empty bytes), Python raises no
exception. The code fell through to `return`, incorrectly reporting "service
is up". `b''` means SLIRP forwarded the connection to the guest, the guest's
TCP stack RST'd (no service listening), and SLIRP converted RST→FIN → the
host sees connection closed. Only `socket.timeout` (remote end holding the
connection open, waiting for client data) and non-empty `data` (banner
received) are genuine ready signals.
**Fix:** Changed `recv(1)` to save the return value. On `socket.timeout`,
return immediately (genuine up). On non-empty `data`, return (banner). On
`b''`, set `last_err` and `continue` (retry).
**Files:** `tools/run_tier3_demo.py`
---
## Bug 11 — `distccd` and `unreal_ircd` incorrectly marked `requires_bridge = true`
**Symptom:** `distcc_exec` and `unreal_ircd_3281_backdoor` were filtered from
`usable_modules` on every SLIRP-only run, even though their `cmd/unix/bind_perl`
payloads create an inward-connecting bind shell (host connects to guest), which
does NOT require the bridge.
**Root cause:** The comment in `distccd_command_exec.toml` said "needs bridge so
the guest can reach the attacker" — correct for reverse_tcp payloads, wrong for
bind_perl. bind_perl listens on the guest; msfrpcd connects to the hostfwd'd
loopback port. No guest egress is needed.
**Fix:** Set `requires_bridge = false` in both modules. The fleet already adds
per-slot hostfwd entries for `extra_target_ports`, so these modules now work on
SLIRP+hostfwd runs without any other change.
**Files:** `exploits/modules/distccd_command_exec.toml`,
`exploits/modules/unreal_ircd_3281_backdoor.toml`
---
## Bug 12 — `msgpack.unpackb` crashes on integer session IDs
**Symptom:** `wait_for_new_session` raises `ValueError: int is not allowed for
map key` when msfrpcd returns a session dict keyed by integer session IDs.
Traceback seen in slot-0 logs on 2026-05-01.
**Root cause:** `msgpack.unpackb(raw, raw=False)` defaults to
`strict_map_key=True`, which rejects non-string keys. Metasploit 6.x msfrpcd
encodes session IDs as msgpack int64 map keys.
**Fix:** Added `strict_map_key=False` to the `unpackb` call in `_raw_call`.
**Files:** `exploits/msfrpc.py`
---
## Bug 13 — `samba_usermap_script` never opens a session (removed from catalog)
**Symptom:** `multi/samba/usermap_script` fired, port 4444 bound in guest, but
Metasploit reported `Rex::Proto::SMB::Exceptions::NoReply` on every run.
`session.list` stayed empty for the full 30 s timeout.
**Root cause:** The SMB auth connection is disrupted when Samba's
`username map script` executes the injected command (smbd kills the auth
handler). Metasploit never received an SMB response → marked exploit "failed"
→ skipped calling the bind-shell handler → session never created.
**Fix:** Removed `samba_usermap_script.toml` from the catalog. The fleet now
uses `distccd_command_exec` and `unreal_ircd_3281_backdoor` as SLIRP-capable
modules (see Bug 11 fix). Both protocols return a proper response after the
exploit fires, so Metasploit's handler is called and sessions open.
**Files:** `exploits/modules/samba_usermap_script.toml` (deleted),
`orchestrator/fleet.py`
---
## Net result after all fixes
With fixes 19 applied:
- All 4 Tier-3 slots use SLIRP+hostfwd with correct per-slot port mapping.
- `samba_usermap_script` fires `cmd/unix/bind_perl` with the correct per-slot
LPORT; msfrpcd connects to the bind port via hostfwd.
- The exploit fires only after Metasploitable2 confirms its login prompt on
the serial console (~60 s after QEMU start).
With fixes 113 applied:
- `_wait_for_tcp` correctly waits until a service is genuinely listening
(returns only on `socket.timeout` or non-empty banner data).
- `distccd_command_exec` and `unreal_ircd_3281_backdoor` are now available
on SLIRP-only runs; `samba_usermap_script` is removed.
- `msgpack.unpackb` accepts integer session ID keys without crashing.
- Sessions open, workloads execute, episodes complete with `session_open`
events (not `session_open_timeout`).
events.

View file

@ -28,9 +28,7 @@ LPORT = 4444
type = "shell"
[runtime]
# Reverse/bind callback path → needs the host-only bridge so the
# guest can reach the attacker (or the host can reach the bind port
# beyond SLIRP's restricted forward). Set BRIDGE=br-malware on the
# lab host to enable.
requires_bridge = true
# 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

@ -24,5 +24,5 @@ LPORT = 4446
type = "shell"
[runtime]
requires_bridge = true
requires_bridge = false
extra_target_ports = [4446]

View file

@ -196,7 +196,7 @@ class MSFRpcClient:
conn.close()
try:
decoded = self._str(msgpack.unpackb(raw, raw=False))
decoded = self._str(msgpack.unpackb(raw, raw=False, strict_map_key=False))
except Exception as e:
raise MSFRpcError(f"could not decode msfrpcd response: {e}") from e

View file

@ -89,10 +89,15 @@ def _wait_for_tcp(host: str, port: int, timeout_s: float) -> None:
# OSError/reset → port closed; retry
s.settimeout(3.0)
try:
s.recv(1)
data = s.recv(1)
except socket.timeout:
pass # service alive, waiting for us to speak first
return
return # service alive, waiting for client data ✓
if data:
return # banner received ✓
# b'' = connection closed by peer (service not ready) → retry
last_err = OSError("connection closed by peer (service not ready)")
time.sleep(1.0)
continue
except OSError as e:
last_err = e
time.sleep(1.0)