Summary
FETCH_COMMAND=TFTP cannot currently be combined with any FETCH_FILELESS mode other than none — the two required options FETCH_WRITABLE_DIR and FETCH_FILENAME are impossible to satisfy at the same time when launching via exploit -j/msfvenom (i.e. any non-interactive path).
Root cause
FETCH_WRITABLE_DIR and FETCH_FILENAME are registered as required: true, with a conditions: entry meant to relax that requirement once fileless mode is active:
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/payload/adapter/fetch/linux_options.rb#L9-L11
Msf::OptString.new('FETCH_FILENAME', [ false, '...', Rex::Text.rand_text_alpha(rand(8..12))], regex: %r{^[^\s/\\]*$}, conditions: ['FETCH_FILELESS', '==', 'none']),
...
Msf::OptString.new('FETCH_WRITABLE_DIR', [ true, '...', './'], regex: /^\S*$/, conditions: ['FETCH_FILELESS', '==', 'none'])
But Msf::OptCondition.show_option — the thing that actually reads conditions: — is only ever called from console-layer helpers (show options, tab-completion, set validation):
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/ui/console/module_option_validation.rb#L33,L43,L70
It is never consulted by the core datastore/module launch-time validator (Msf::OptBase#valid?, used by exploit -j / msfvenom / RPC). That validator only looks at the option's static required flag:
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/opt_base.rb#L119-L123
def valid?(value, check_empty: true, datastore: nil)
if check_empty && required?
return false if (value.nil? || value.to_s.empty?)
end
...
So at launch time FETCH_WRITABLE_DIR is always required to be non-empty, regardless of FETCH_FILELESS.
Meanwhile, the TFTP-specific fetch-command generator explicitly requires the opposite whenever it runs:
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/payload/adapter/fetch.rb#L281-L287
def _check_tftp_file
# Older Linux tftp clients do not support saving the file under a different name
unless datastore['FETCH_WRITABLE_DIR'].blank? && datastore['FETCH_FILENAME'].blank?
print_error('The Linux TFTP client does not support saving a file under a different name than the URI.')
fail_with(Msf::Module::Failure::BadConfig, 'FETCH_WRITABLE_DIR and FETCH_FILENAME must be blank when using the tftp client')
end
end
The two checks contradict each other for FETCH_COMMAND=TFTP combined with FETCH_FILELESS != none: the launch validator rejects a blank FETCH_WRITABLE_DIR, and _check_tftp_file rejects a non-blank one. There is no value that satisfies both.
Reproduction
msf6 > use exploit/multi/handler
msf6 exploit(multi/handler) > set PAYLOAD cmd/linux/tftp/x64/meterpreter_reverse_tcp
msf6 exploit(multi/handler) > set LHOST <attacker>
msf6 exploit(multi/handler) > set FETCH_SRVHOST <attacker>
msf6 exploit(multi/handler) > set FETCH_SRVPORT 69
msf6 exploit(multi/handler) > set FETCH_COMMAND TFTP
msf6 exploit(multi/handler) > set FETCH_FILELESS shell-search
msf6 exploit(multi/handler) > exploit -j -z
[-] Msf::OptionValidateError One or more options failed to validate: FETCH_WRITABLE_DIR.
Manually blanking the option (unset --clear FETCH_WRITABLE_DIR FETCH_FILENAME) gets past that error, but then hits _check_tftp_file's own rejection instead:
msf6 exploit(multi/handler) > set --clear FETCH_WRITABLE_DIR
msf6 exploit(multi/handler) > set --clear FETCH_FILENAME
msf6 exploit(multi/handler) > exploit -j -z
[-] Exploit failed: bad-config: FETCH_WRITABLE_DIR and FETCH_FILENAME must be blank when using the tftp client
i.e. FETCH_COMMAND=TFTP + fileless mode is unreachable through the normal set/exploit -j/msfvenom flow at all. (It's only reachable by poking datastore[...] directly from irb, bypassing option validation entirely — which is how I worked around it to confirm the underlying fetch/cleanup logic in #21548 was otherwise correct.)
Suggested fix
Msf::OptBase#valid? (or its caller in the module/datastore validation path) should consult conditions: before enforcing required?, the same way the console layer already does via Msf::OptCondition.show_option. Alternatively, as a narrower fix scoped to this module family, FETCH_WRITABLE_DIR/FETCH_FILENAME could register as required: false and have _check_tftp_file (and the equivalent non-TFTP paths) do their own presence/blankness enforcement based on FETCH_FILELESS, rather than relying on the base option's required: flag at all.
Environment
Found while testing #21548 (fail-safe fileless fetch payloads) against real hardware. Not a bug introduced by that PR — the conditions: metadata and this validation gap both predate it.
Summary
FETCH_COMMAND=TFTPcannot currently be combined with anyFETCH_FILELESSmode other thannone— the two required optionsFETCH_WRITABLE_DIRandFETCH_FILENAMEare impossible to satisfy at the same time when launching viaexploit -j/msfvenom(i.e. any non-interactive path).Root cause
FETCH_WRITABLE_DIRandFETCH_FILENAMEare registered asrequired: true, with aconditions:entry meant to relax that requirement once fileless mode is active:https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/payload/adapter/fetch/linux_options.rb#L9-L11
But
Msf::OptCondition.show_option— the thing that actually readsconditions:— is only ever called from console-layer helpers (show options, tab-completion,setvalidation):https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/ui/console/module_option_validation.rb#L33,L43,L70
It is never consulted by the core datastore/module launch-time validator (
Msf::OptBase#valid?, used byexploit -j/msfvenom/ RPC). That validator only looks at the option's staticrequiredflag:https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/opt_base.rb#L119-L123
So at launch time
FETCH_WRITABLE_DIRis always required to be non-empty, regardless ofFETCH_FILELESS.Meanwhile, the TFTP-specific fetch-command generator explicitly requires the opposite whenever it runs:
https://github.com/rapid7/metasploit-framework/blob/master/lib/msf/core/payload/adapter/fetch.rb#L281-L287
The two checks contradict each other for
FETCH_COMMAND=TFTPcombined withFETCH_FILELESS != none: the launch validator rejects a blankFETCH_WRITABLE_DIR, and_check_tftp_filerejects a non-blank one. There is no value that satisfies both.Reproduction
Manually blanking the option (
unset --clear FETCH_WRITABLE_DIR FETCH_FILENAME) gets past that error, but then hits_check_tftp_file's own rejection instead:i.e.
FETCH_COMMAND=TFTP+ fileless mode is unreachable through the normalset/exploit -j/msfvenomflow at all. (It's only reachable by pokingdatastore[...]directly fromirb, bypassing option validation entirely — which is how I worked around it to confirm the underlying fetch/cleanup logic in #21548 was otherwise correct.)Suggested fix
Msf::OptBase#valid?(or its caller in the module/datastore validation path) should consultconditions:before enforcingrequired?, the same way the console layer already does viaMsf::OptCondition.show_option. Alternatively, as a narrower fix scoped to this module family,FETCH_WRITABLE_DIR/FETCH_FILENAMEcould register asrequired: falseand have_check_tftp_file(and the equivalent non-TFTP paths) do their own presence/blankness enforcement based onFETCH_FILELESS, rather than relying on the base option'srequired:flag at all.Environment
Found while testing #21548 (fail-safe fileless fetch payloads) against real hardware. Not a bug introduced by that PR — the
conditions:metadata and this validation gap both predate it.