Skip to content

Commit 11ad4a3

Browse files
committed
Spike Windows version checks in exploit targets and payloads
1 parent 464c808 commit 11ad4a3

25 files changed

Lines changed: 559 additions & 43 deletions

lib/msf/base/serializer/readable_text.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -492,13 +492,20 @@ def self.dump_evasion_module(mod, indent = '')
492492
def self.dump_payload_module(mod, indent = '')
493493
# General
494494
output = "\n"
495-
output << " Name: #{mod.name}\n"
496-
output << " Module: #{mod.fullname}\n"
497-
output << " Platform: #{mod.platform_to_s}\n"
498-
output << " Arch: #{mod.arch_to_s}\n"
499-
output << "Needs Admin: " + (mod.privileged? ? "Yes" : "No") + "\n"
500-
output << " Total size: #{mod.size}\n"
501-
output << " Rank: #{mod.rank_to_s.capitalize}\n"
495+
output << " Name: #{mod.name}\n"
496+
output << " Module: #{mod.fullname}\n"
497+
output << " Platform: #{mod.platform_to_s}\n"
498+
output << " Arch: #{mod.arch_to_s}\n"
499+
output << " Needs Admin: " + (mod.privileged? ? "Yes" : "No") + "\n"
500+
output << " Total size: #{mod.size}\n"
501+
output << " Rank: #{mod.rank_to_s.capitalize}\n"
502+
503+
required_versions = mod.instance_variable_get(:@module_info)['MinimumVersions']
504+
if required_versions && required_versions.any?
505+
output << "Required Versions:\n"
506+
# No access to a friendly version name here
507+
required_versions.map { |k, v| output << " #{k}: #{v}\n" }
508+
end
502509
output << "\n"
503510

504511
# Authors

lib/msf/core/module.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ class Module
5858
include Msf::Module::Ranking
5959
include Msf::Module::Type
6060
include Msf::Module::UI
61+
include Msf::Module::VersionCompatibility
6162
include Msf::Module::UUID
6263
include Msf::Module::SideEffects
6364
include Msf::Module::Stability
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/version'
4+
5+
#
6+
# Provides version compatibility checks between exploit targets and payloads.
7+
#
8+
module Msf::Module::VersionCompatibility
9+
# Check version compatibility between a payload and the current exploit target.
10+
#
11+
# @param payload_instance [Msf::Payload] An payload module instance.
12+
# @return [Array<String>] An array of warning strings. Empty if there were no warnings and payload is compatible.
13+
def version_compatibility_warnings(payload_instance)
14+
warnings = []
15+
16+
target_versions = current_target_runtime_versions
17+
return warnings unless target_versions.is_a?(Hash) && !target_versions.empty?
18+
19+
payload_mins = payload_minimum_versions(payload_instance)
20+
return warnings unless payload_mins.is_a?(Hash) && !payload_mins.empty?
21+
22+
payload_mins.each do |runtime, min_version|
23+
next unless target_versions.key?(runtime)
24+
25+
target_ver = to_version(target_versions[runtime])
26+
required_ver = to_version(min_version)
27+
28+
if target_ver < required_ver
29+
required_name = human_readable_version_string(runtime, required_ver)
30+
target_name = human_readable_version_string(runtime, target_ver)
31+
warnings << "Payload requires #{runtime} >= #{required_name}, but the minimum potentially provided by the target is #{target_name}"
32+
end
33+
end
34+
35+
warnings
36+
end
37+
38+
private
39+
40+
# Normalize a value to Rex::Version
41+
#
42+
# @param value [String, Rex::Version] The version to normalize.
43+
# @return [Rex::Version]
44+
def to_version(value)
45+
value.is_a?(Rex::Version) ? value : Rex::Version.new(value.to_s)
46+
end
47+
48+
# Look up a human-readable name for a version number.
49+
# Falls back to the raw version string if no mapping is found.
50+
#
51+
# @param runtime [String] The runtime key (e.g., 'Windows', 'Python').
52+
# @param version [Rex::Version] The version to look up.
53+
# @return [String] A human-readable string like "Windows XP Service Pack 2 (5.1.2600.2)"
54+
def human_readable_version_string(runtime, version)
55+
case runtime
56+
when 'Windows'
57+
name = windows_version_name(version)
58+
return "#{name} (#{version})" if name
59+
end
60+
61+
version.to_s
62+
end
63+
64+
# Look up a Windows version's human-readable name from the WindowsVersion mappings.
65+
#
66+
# @param version [Rex::Version] The version to look up.
67+
# @return [String, nil] The friendly name, or nil if not found.
68+
def windows_version_name(version)
69+
[
70+
{ klass: Msf::WindowsVersion::WorkstationSpecificVersions, mapping: Msf::WindowsVersion::WorkstationNameMapping },
71+
{ klass: Msf::WindowsVersion::ServerSpecificVersions, mapping: Msf::WindowsVersion::ServerNameMapping }
72+
].each do |h|
73+
h[:klass].constants.each do |const|
74+
return h[:mapping][const] if h[:klass].const_get(const) == version
75+
end
76+
end
77+
78+
nil
79+
end
80+
81+
# Retrieve RuntimeVersions from the currently selected target.
82+
# If the current target does not declare RuntimeVersions but other targets do,
83+
# returns the lowest (most conservative) version across all targets that declare them.
84+
# This handles the "Automatic" target case where the exploit may end up running
85+
# against the lowest-versioned target at runtime.
86+
#
87+
# @return [Hash, nil] The RuntimeVersions hash from the active target, or nil.
88+
def current_target_runtime_versions
89+
return nil unless respond_to?(:target) && target
90+
91+
# If the selected target explicitly declares RuntimeVersions, use those directly
92+
target_versions = target.opts['RuntimeVersions']
93+
if target_versions.is_a?(Hash) && !target_versions.empty?
94+
return target_versions
95+
end
96+
97+
# Only compute the lowest common denominator for targets that are explicitly
98+
# automatic (i.e., have no RuntimeVersions and are flagged as auto or named "Automatic").
99+
# This avoids incorrectly triggering the fallback for non-auto targets that simply
100+
# haven't been annotated with RuntimeVersions yet.
101+
is_auto_target = target.opts['auto'] || target.name =~ /Automatic/i
102+
return nil unless is_auto_target
103+
104+
# For automatic targets, compute the lowest version across all other targets
105+
# that declare RuntimeVersions. This represents the worst-case scenario at runtime.
106+
return nil unless respond_to?(:targets) && targets.is_a?(Array)
107+
108+
lowest_versions = {}
109+
targets.each do |t|
110+
rt = t.opts['RuntimeVersions']
111+
next unless rt.is_a?(Hash)
112+
113+
rt.each do |runtime, version|
114+
ver = to_version(version)
115+
if lowest_versions[runtime].nil? || ver < lowest_versions[runtime]
116+
lowest_versions[runtime] = ver
117+
end
118+
end
119+
end
120+
121+
lowest_versions.empty? ? nil : lowest_versions
122+
end
123+
124+
# Retrieve MinimumVersions from a payload instance.
125+
#
126+
# @param payload_instance [Msf::Payload] The payload to inspect.
127+
# @return [Hash, nil] The MinimumVersions hash with OS names as the keys, or nil.
128+
def payload_minimum_versions(payload_instance)
129+
payload_instance.instance_variable_get(:@module_info)&.dig('MinimumVersions')
130+
end
131+
end

lib/msf/core/payload.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,13 @@ def self.choose_payload(mod)
530530

531531
next unless payload
532532

533+
# Skip payloads that don't meet the target's version requirements
534+
payload_instance = mod.framework.payloads.create(payload)
535+
if payload_instance
536+
warnings = mod.version_compatibility_warnings(payload_instance)
537+
next unless warnings.empty?
538+
end
539+
533540
return configure_payload.call(payload)
534541
end
535542

lib/msf/core/payload/python/meterpreter_loader.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: binary -*-
22

3+
require 'msf/core/payload/python/meterpreter_version'
34

45
module Msf
56

@@ -25,7 +26,8 @@ def initialize(info = {})
2526
'Author' => [ 'Spencer McIntyre' ],
2627
'Platform' => 'python',
2728
'Arch' => ARCH_PYTHON,
28-
'Stager' => {'Payload' => ""}
29+
'Stager' => {'Payload' => ""},
30+
'MinimumVersions' => { 'Python' => Msf::Payload::Python::MeterpreterVersion::MINIMUM_VERSION }
2931
))
3032

3133
register_advanced_options(
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
5+
module Payload::Python::MeterpreterVersion
6+
# The minimum version of Python that is able to run Meterpreter payloads. Versions below this are not supported.
7+
MINIMUM_VERSION = '2.5'
8+
end
9+
10+
end

lib/msf/core/payload/windows.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
###
1111
module Msf::Payload::Windows
1212

13+
require 'msf/core/payload/windows/meterpreter_version'
1314

1415
# Provides the #prepends method
1516
# XXX: For some unfathomable reason, the order of requires here is

lib/msf/core/payload/windows/meterpreter_loader.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: binary -*-
22

3-
43
module Msf
54

65
###
@@ -26,7 +25,8 @@ def initialize(info = {})
2625
'Platform' => 'win',
2726
'Arch' => ARCH_X86,
2827
'PayloadCompat' => { 'Convention' => 'sockedi handleedi -https', },
29-
'Stage' => { 'Payload' => "" }
28+
'Stage' => { 'Payload' => "" },
29+
'MinimumVersions' => { 'Windows' => Msf::Payload::Windows::MeterpreterVersion::MINIMUM_VERSION }
3030
))
3131
end
3232

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# -*- coding: binary -*-
2+
3+
module Msf
4+
5+
module Payload::Windows::MeterpreterVersion
6+
# The minimum version of Windows that is able to run Meterpreter payloads. Versions below this are not supported.
7+
MINIMUM_VERSION = Msf::WindowsVersion::XP_SP2
8+
end
9+
10+
end

lib/msf/core/payload/windows/x64/meterpreter_loader_x64.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# -*- coding: binary -*-
22

3-
43
module Msf
54

65
###
@@ -27,7 +26,8 @@ def initialize(info = {})
2726
'Platform' => 'win',
2827
'Arch' => ARCH_X64,
2928
'PayloadCompat' => { 'Convention' => 'sockrdi handlerdi -https' },
30-
'Stage' => { 'Payload' => "" }
29+
'Stage' => { 'Payload' => "" },
30+
'MinimumVersions' => { 'Windows' => Msf::Payload::Windows::MeterpreterVersion::MINIMUM_VERSION }
3131
))
3232
end
3333

0 commit comments

Comments
 (0)