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:
parent
3d4f282e9c
commit
b73f5559dc
5 changed files with 101 additions and 17 deletions
|
|
@ -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
|
## Net result after all fixes
|
||||||
|
|
||||||
With fixes 1–9 applied:
|
With fixes 1–13 applied:
|
||||||
- All 4 Tier-3 slots use SLIRP+hostfwd with correct per-slot port mapping.
|
- `_wait_for_tcp` correctly waits until a service is genuinely listening
|
||||||
- `samba_usermap_script` fires `cmd/unix/bind_perl` with the correct per-slot
|
(returns only on `socket.timeout` or non-empty banner data).
|
||||||
LPORT; msfrpcd connects to the bind port via hostfwd.
|
- `distccd_command_exec` and `unreal_ircd_3281_backdoor` are now available
|
||||||
- The exploit fires only after Metasploitable2 confirms its login prompt on
|
on SLIRP-only runs; `samba_usermap_script` is removed.
|
||||||
the serial console (~60 s after QEMU start).
|
- `msgpack.unpackb` accepts integer session ID keys without crashing.
|
||||||
- Sessions open, workloads execute, episodes complete with `session_open`
|
- Sessions open, workloads execute, episodes complete with `session_open`
|
||||||
events (not `session_open_timeout`).
|
events.
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ LPORT = 4444
|
||||||
type = "shell"
|
type = "shell"
|
||||||
|
|
||||||
[runtime]
|
[runtime]
|
||||||
# Reverse/bind callback path → needs the host-only bridge so the
|
# bind_perl opens a new guest port; fleet hostfwds it via SLIRP.
|
||||||
# guest can reach the attacker (or the host can reach the bind port
|
# No bridge egress needed — host connects in, not guest out.
|
||||||
# beyond SLIRP's restricted forward). Set BRIDGE=br-malware on the
|
requires_bridge = false
|
||||||
# lab host to enable.
|
|
||||||
requires_bridge = true
|
|
||||||
extra_target_ports = [4444]
|
extra_target_ports = [4444]
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,5 @@ LPORT = 4446
|
||||||
type = "shell"
|
type = "shell"
|
||||||
|
|
||||||
[runtime]
|
[runtime]
|
||||||
requires_bridge = true
|
requires_bridge = false
|
||||||
extra_target_ports = [4446]
|
extra_target_ports = [4446]
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,7 @@ class MSFRpcClient:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as e:
|
||||||
raise MSFRpcError(f"could not decode msfrpcd response: {e}") from e
|
raise MSFRpcError(f"could not decode msfrpcd response: {e}") from e
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,15 @@ def _wait_for_tcp(host: str, port: int, timeout_s: float) -> None:
|
||||||
# OSError/reset → port closed; retry
|
# OSError/reset → port closed; retry
|
||||||
s.settimeout(3.0)
|
s.settimeout(3.0)
|
||||||
try:
|
try:
|
||||||
s.recv(1)
|
data = s.recv(1)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
pass # service alive, waiting for us to speak first
|
return # service alive, waiting for client data ✓
|
||||||
return
|
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:
|
except OSError as e:
|
||||||
last_err = e
|
last_err = e
|
||||||
time.sleep(1.0)
|
time.sleep(1.0)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue