From b73f5559dcd4597002bfe34c0f71b38a1857df66 Mon Sep 17 00:00:00 2001 From: Elliott Kolden Date: Tue, 5 May 2026 15:15:18 -0600 Subject: [PATCH] Tier-3 fixes: b'' probe false-positive, requires_bridge, msgpack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- TIER3-BRINGUP.md | 95 +++++++++++++++++-- exploits/modules/distccd_command_exec.toml | 8 +- .../modules/unreal_ircd_3281_backdoor.toml | 2 +- exploits/msfrpc.py | 2 +- tools/run_tier3_demo.py | 11 ++- 5 files changed, 101 insertions(+), 17 deletions(-) diff --git a/TIER3-BRINGUP.md b/TIER3-BRINGUP.md index 7bc54e9..12fa397 100644 --- a/TIER3-BRINGUP.md +++ b/TIER3-BRINGUP.md @@ -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 1–9 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 1–13 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. diff --git a/exploits/modules/distccd_command_exec.toml b/exploits/modules/distccd_command_exec.toml index d08c693..83eaa4b 100644 --- a/exploits/modules/distccd_command_exec.toml +++ b/exploits/modules/distccd_command_exec.toml @@ -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] diff --git a/exploits/modules/unreal_ircd_3281_backdoor.toml b/exploits/modules/unreal_ircd_3281_backdoor.toml index f795ba1..3721356 100644 --- a/exploits/modules/unreal_ircd_3281_backdoor.toml +++ b/exploits/modules/unreal_ircd_3281_backdoor.toml @@ -24,5 +24,5 @@ LPORT = 4446 type = "shell" [runtime] -requires_bridge = true +requires_bridge = false extra_target_ports = [4446] diff --git a/exploits/msfrpc.py b/exploits/msfrpc.py index e408357..56ba13b 100644 --- a/exploits/msfrpc.py +++ b/exploits/msfrpc.py @@ -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 diff --git a/tools/run_tier3_demo.py b/tools/run_tier3_demo.py index 2e76078..a9e1438 100644 --- a/tools/run_tier3_demo.py +++ b/tools/run_tier3_demo.py @@ -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)