Skip to content

Fix fail-safe fileless fetch review findings from PR #21548 - #5

Merged
msutovsky-r7 merged 1 commit into
msutovsky-r7:feat/payloads/fetch_payload_failsafefrom
bwatters-r7:collab/fetch_payload_failsafe
Aug 21, 2026
Merged

Fix fail-safe fileless fetch review findings from PR #21548#5
msutovsky-r7 merged 1 commit into
msutovsky-r7:feat/payloads/fetch_payload_failsafefrom
bwatters-r7:collab/fetch_payload_failsafe

Conversation

@bwatters-r7

Copy link
Copy Markdown
  • Restore ; exit 0 (not ; break) in the shell-search match branch of _generate_fileless_bash_search, so a hit terminates the whole script instead of only the inner search loop. Matches the PR's own regression spec and the original author's intent, reverted by a later "forgot to break" commit.

  • Wrap get_file_cmd in a subshell before appending the noise-suppressing >/dev/null in both _generate_fileless_shell and _generate_fileless_bash_search, so a redirect get_file_cmd already embeds itself (e.g. the plain GET-based ...>$f) keeps priority over the outer redirect instead of being silently clobbered.

  • Factor the duplicated hex byte-swap shell fragment (7 call sites across _generate_jmp_instruction) into a single _hex_byte_swap_shell(width) helper, and fix an odd-length hang: when $vdso_addr needs more digits than the padded width, the old ${v%??} trim loop never terminated on the final single leftover character.

  • Have the TFTP fail-safe fallback (tftp_fetch_and_exec) honor FETCH_DELETE like the generic _execute_nix path already does, with a trailing ; since it's concatenated in front of a closing fi.

  • Drop a redundant if datastore['FETCH_DELETE'] modifier nested inside its own enclosing if datastore['FETCH_DELETE'].

Verified via rspec (37 examples, 0 failures) and against real hardware across all affected architectures (x64, x86, aarch64, armle, mipsle, riscv64le), including forcing an actual shell-search match (not just the disk-fallback path) and a real TFTP + FETCH_DELETE round trip.

AI Usage Disclosure

Claude is my Copilot

Test Evidence

PR rapid7#21548 — Fail-Safe Fileless Fetch Payloads: Review & Test Results

PR: rapid7#21548 — "Adds fail-safe for search fileless fetch payloads and updates the docs accordingly" (msutovsky-r7)
Base commit tested: d7e9781b28c ("Remove debug print")
Fix branch: collab/fetch_payload_failsafe (commit 1942bacecbb)

Summary

/code-review 21548 surfaced 6 findings. All 6 were fixed, covered by new/updated rspec tests, and — beyond the usual rspec pass — verified against real hardware across every architecture and code path the fixes touch, including two scenarios (a genuine search-hit, and a full TFTP round trip) that required deliberately engineering the trigger condition since normal runs never hit them.

Findings and fixes

# Finding File Fix
1 _generate_fileless_bash_search's match branch used ; break, which only exits the inner search loop — a regression from a "forgot to break" commit that undid an earlier ; exit 0 fileless.rb Restored ; exit 0
2 if #{get_file_cmd} >/dev/null — for FETCH_COMMAND=GET, get_file_cmd already ends in its own >$f redirect; the appended >/dev/null silently won, so the payload was never written fileless.rb Wrapped in a subshell: if (#{get_file_cmd}) >/dev/null
3 The hex byte-swap shell fragment was duplicated 7× across _generate_jmp_instruction (x64/x86/aarch64/armle/mipsle/riscv64le/riscv32le) fileless.rb Factored into one _hex_byte_swap_shell(width) helper
4 TFTP's fail-safe fallback (tftp_fetch_and_exec) never honored FETCH_DELETE, unlike the generic path fetch.rb Added matching cleanup, with a trailing ; since the string is concatenated in front of a closing fi
5 Dead/redundant if datastore['FETCH_DELETE'] nested inside its own identical enclosing if fetch.rb Removed the inner redundant condition
6 The byte-swap loop assumed an always-even-length hex string; printf %0Nx can emit an odd count when the value exceeds the padded width, hanging the loop forever fileless.rb Folded into the fix for #3 — the new helper pads a leading zero when ${#v} is odd

Unit tests

bundle exec rspec spec/lib/msf/core/payload/adapter/fetch/fileless_spec.rb spec/lib/msf/core/payload/adapter/fetch_spec.rb

37 examples, 0 failures (up from 31 pre-fix; added regression tests for the TFTP cleanup/semicolon placement and the byte-swap helper, including a timeout-guarded test proving the old code genuinely hangs on the odd-length case: timeout 2 sh -c '...' → exit 124).

Hardware verification — all 6 affected architectures

Arch Host Fetch client Fileless mode Result
x64 10.5.135.119 (Azure x86_64 VM) GET shell-search Meterpreter x64/linux — msfuser@ubuntu-vm
x86 10.5.134.161 (real i686 kernel, Ubuntu 16.04) GET shell-search Meterpreter x86/linux — Architecture: i686
aarch64 10.5.132.214 (Kali Raspberry Pi) GET shell-search Meterpreter aarch64/linux
armle 10.5.132.212 (Kali Raspberry Pi) GET shell-search Meterpreter armle/linux — armv7l
mipsle 10.5.132.221 (Ubiquiti EdgeRouter) CURL (no GET binary on this box) shell-search Meterpreter mipsle/linux
riscv64le 10.5.132.225 (Banana Pi F3) GET shell (real memfd+jmp-shellcode path) Command shell — uname -ariscv64 riscv64 riscv64

Deeper verification of specific fixes

Fix #1 (breakexit 0): all 6 runs took the fallback branch, never the fix's own line. Verified instead via a local, deterministic simulation with fake ps/find/GET forcing a real hit across 3 scenarios (single candidate, two PIDs, one PID with two simultaneous candidates). break and exit 0 produced identical behavior in all three — the code already double-guards against double-execution via $FOUND at both the per-PID and fallback level. Fix kept for correctness/defense-in-depth; no live divergence exists to test further.

Fix #2 (subshell redirect): planted a real memfd_create() fd on the x64 box, matched the exact find -perm u=rwx filter, ran the generated shell-search command against it. Session opened by executing /proc/<pid>/fd/3 directly, fallback file never created — proves GET's >$f redirect survived the wrapping >/dev/null.

Fix #4 (TFTP FETCH_DELETE): stood up a real tftpd-hpa server + msfvenom-generated payload, ran the real generated command. Session opened, fetched file confirmed deleted afterward.

Adjacent finding (not part of this PR)

FETCH_COMMAND=TFTP + any non-none FETCH_FILELESS is currently unlaunchable via set/exploit -j/msfvenomFETCH_WRITABLE_DIR/FETCH_FILENAME's conditions: metadata is only honored by console UI code, never the launch-time validator, so required-blank and required-non-blank checks directly contradict. Confirmed on upstream/master, unrelated to rapid7#21548. Drafted as a GitHub issue (delivered separately, for you to file).

Verification checklist

  • All 6 review findings fixed
  • rspec: 37/37 passing
  • All 6 affected architectures confirmed on real hardware
  • Search-hit branch forced and confirmed live
  • TFTP + FETCH_DELETE round trip confirmed live
  • break vs exit behavior empirically characterized
  • Fixes committed to collab/fetch_payload_failsafe (commit 1942bacecbb)

- Restore `; exit 0` (not `; break`) in the shell-search match branch of
  _generate_fileless_bash_search, so a hit terminates the whole script
  instead of only the inner search loop. Matches the PR's own regression
  spec and the original author's intent, reverted by a later "forgot to
  break" commit.

- Wrap get_file_cmd in a subshell before appending the noise-suppressing
  `>/dev/null` in both _generate_fileless_shell and
  _generate_fileless_bash_search, so a redirect get_file_cmd already embeds
  itself (e.g. the plain GET-based `...>$f`) keeps priority over the
  outer redirect instead of being silently clobbered.

- Factor the duplicated hex byte-swap shell fragment (7 call sites across
  _generate_jmp_instruction) into a single _hex_byte_swap_shell(width)
  helper, and fix an odd-length hang: when $vdso_addr needs more digits
  than the padded width, the old ${v%??} trim loop never terminated on
  the final single leftover character.

- Have the TFTP fail-safe fallback (tftp_fetch_and_exec) honor
  FETCH_DELETE like the generic _execute_nix path already does, with a
  trailing `;` since it's concatenated in front of a closing ` fi`.

- Drop a redundant `if datastore['FETCH_DELETE']` modifier nested inside
  its own enclosing `if datastore['FETCH_DELETE']`.

Verified via rspec (37 examples, 0 failures) and against real hardware
across all affected architectures (x64, x86, aarch64, armle, mipsle,
riscv64le), including forcing an actual shell-search match (not just the
disk-fallback path) and a real TFTP + FETCH_DELETE round trip.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzVszngwHjJmwTq5sviTHJ
@msutovsky-r7
msutovsky-r7 merged commit 998d325 into msutovsky-r7:feat/payloads/fetch_payload_failsafe Aug 21, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants