Skip to content

Commit 16407f9

Browse files
committed
Rescue Errno::ENOENT from File.open in read_module_content
[Fixes #38426061, #38097411] Msf::Modules::Loader::Directory#read_module_content may calculate a non-existent module_path that gets passed to File.open causing an Errno::ENOENT exception to be raised when using the module cache with a module that has been moved to a new path (as is the case that originally found this bug) or deleted. Now, the exception is rescued and read_module_content returns an empty string (''), which load_module detects with module_content.empty? and returns earlier without attempting to module eval the (empty) content. As having Msf::Modules::Loader::Directory#read_module_content rescue the exception, meant there was another place that needed to log and error and store an error in Msf::ModuleManager#module_load_error_by_path, I refactored the error reporting to call Msf::Modules::Loader::Base#load_error, which handles writing to the log and setting the Hash, so the error reporting is consistent across the loaders. The exception hierarchy was also refactored so that namespace_module.metasploit_class now has an error raising counter-part: namespace_module.metasploit_class! that can be used with Msf::Modules::Loader::Base#load_error as it requires an exception, and not just a string so the exception class, message, and backtrace can be logged.
1 parent 236db52 commit 16407f9

16 files changed

+827
-96
lines changed

lib/msf/core/modules/error.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Base error class for all error under {Msf::Modules}
2+
class Msf::Modules::Error < StandardError
3+
def initialize(attributes={})
4+
@module_path = attributes[:module_path]
5+
@module_reference_name = attributes[:module_reference_name]
6+
7+
message_parts = []
8+
message_parts << "Failed to load module"
9+
10+
if module_reference_name or module_path
11+
clause_parts = []
12+
13+
if module_reference_name
14+
clause_parts << module_reference_name
15+
end
16+
17+
if module_path
18+
clause_parts << "from #{module_path}"
19+
end
20+
21+
clause = clause_parts.join(' ')
22+
message_parts << "(#{clause})"
23+
end
24+
25+
causal_message = attributes[:causal_message]
26+
27+
if causal_message
28+
message_parts << "due to #{causal_message}"
29+
end
30+
31+
message = message_parts.join(' ')
32+
33+
super(message)
34+
end
35+
36+
attr_reader :module_reference_name
37+
attr_reader :module_path
38+
end

lib/msf/core/modules/loader/base.rb

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#
44
require 'msf/core/modules/loader'
55
require 'msf/core/modules/namespace'
6+
require 'msf/core/modules/metasploit_class_compatibility_error'
67
require 'msf/core/modules/version_compatibility_error'
78

89
# Responsible for loading modules for {Msf::ModuleManager}.
@@ -117,12 +118,17 @@ def load_module(parent_path, type, module_reference_name, options={})
117118

118119
metasploit_class = nil
119120

121+
module_content = read_module_content(parent_path, type, module_reference_name)
122+
123+
if module_content.empty?
124+
# read_module_content is responsible for calling {#load_error}, so just return here.
125+
return false
126+
end
127+
120128
loaded = namespace_module_transaction(type + "/" + module_reference_name, :reload => reload) { |namespace_module|
121129
# set the parent_path so that the module can be reloaded with #load_module
122130
namespace_module.parent_path = parent_path
123131

124-
module_content = read_module_content(parent_path, type, module_reference_name)
125-
126132
begin
127133
namespace_module.module_eval_with_lexical_scope(module_content, module_path)
128134
# handle interrupts as pass-throughs unlike other Exceptions so users can bail with Ctrl+C
@@ -133,45 +139,33 @@ def load_module(parent_path, type, module_reference_name, options={})
133139
begin
134140
namespace_module.version_compatible!(module_path, module_reference_name)
135141
rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error
136-
error_message = "Failed to load module (#{module_path}) due to error and #{version_compatibility_error}"
142+
load_error(module_path, version_compatibility_error)
137143
else
138-
error_message = "#{error.class} #{error}"
144+
load_error(module_path, error)
139145
end
140146

141-
# record the error message without the backtrace for the console
142-
module_manager.module_load_error_by_path[module_path] = error_message
143-
144-
error_message_with_backtrace = "#{error_message}:\n#{error.backtrace.join("\n")}"
145-
elog(error_message_with_backtrace)
146-
147147
return false
148148
end
149149

150150
begin
151151
namespace_module.version_compatible!(module_path, module_reference_name)
152152
rescue Msf::Modules::VersionCompatibilityError => version_compatibility_error
153-
error_message = version_compatibility_error.to_s
154-
155-
elog(error_message)
156-
module_manager.module_load_error_by_path[module_path] = error_message
153+
load_error(module_path, version_compatibility_error)
157154

158155
return false
159156
end
160157

161-
metasploit_class = namespace_module.metasploit_class
162-
163-
unless metasploit_class
164-
error_message = "Missing Metasploit class constant"
165-
166-
elog(error_message)
167-
module_manager.module_load_error_by_path[module_path] = error_message
158+
begin
159+
metasploit_class = namespace_module.metasploit_class!(module_path, module_reference_name)
160+
rescue Msf::Modules::MetasploitClassCompatibilityError => error
161+
load_error(module_path, error)
168162

169-
return false
163+
return false
170164
end
171165

172166
unless usable?(metasploit_class)
173167
ilog(
174-
"Skipping module #{module_reference_name} under #{parent_path} because is_usable returned false.",
168+
"Skipping module (#{module_reference_name} from #{module_path}) because is_usable returned false.",
175169
'core',
176170
LEV_1
177171
)
@@ -409,6 +403,28 @@ def each_module_reference_name(path)
409403
raise ::NotImplementedError
410404
end
411405

406+
# Records the load error to {Msf::ModuleManager::Loading#module_load_error_by_path} and the log.
407+
#
408+
# @param [String] module_path Path to the module as returned by {#module_path}.
409+
# @param [Exception, #class, #to_s, #backtrace] error the error that cause the module not to load.
410+
# @return [void]
411+
#
412+
# @see #module_path
413+
def load_error(module_path, error)
414+
# module_load_error_by_path does not get the backtrace because the value is echoed to the msfconsole where
415+
# backtraces should not appear.
416+
module_manager.module_load_error_by_path[module_path] = "#{error.class} #{error}"
417+
418+
log_lines = []
419+
log_lines << "#{module_path} failed to load due to the following error:"
420+
log_lines << error.class.to_s
421+
log_lines << error.to_s
422+
log_lines += error.backtrace
423+
424+
log_message = log_lines.join("\n")
425+
elog(log_message)
426+
end
427+
412428
# @return [Msf::ModuleManager] The module manager for which this loader is loading modules.
413429
attr_reader :module_manager
414430

lib/msf/core/modules/loader/directory.rb

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,17 @@ def read_module_content(parent_path, type, module_reference_name)
7575

7676
module_content = ''
7777

78-
# force to read in binary mode so Pro modules won't be truncated on Windows
79-
File.open(full_path, 'rb') do |f|
80-
# Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows.
81-
# @see http://www.ruby-forum.com/topic/209005
82-
# @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205
83-
# @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038
84-
module_content = f.read(f.stat.size)
78+
begin
79+
# force to read in binary mode so Pro modules won't be truncated on Windows
80+
File.open(full_path, 'rb') do |f|
81+
# Pass the size of the file as it leads to faster reads due to fewer buffer resizes. Greatest effect on Windows.
82+
# @see http://www.ruby-forum.com/topic/209005
83+
# @see https://github.com/ruby/ruby/blob/ruby_1_8_7/io.c#L1205
84+
# @see https://github.com/ruby/ruby/blob/ruby_1_9_3/io.c#L2038
85+
module_content = f.read(f.stat.size)
86+
end
87+
rescue Errno::ENOENT => error
88+
load_error(full_path, error)
8589
end
8690

8791
module_content
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
require 'msf/core/modules/error'
2+
3+
# Error raised by {Msf::Modules::Namespace#metasploit_class!} if it cannot the namespace_module does not have a constant
4+
# with {Msf::Framework::Major} or lower as a number after 'Metasploit', which indicates a compatible Msf::Module.
5+
class Msf::Modules::MetasploitClassCompatibilityError < Msf::Modules::Error
6+
def initialize(attributes={})
7+
super_attributes = {
8+
:causal_message => 'Missing compatible Metasploit<major_version> class constant',
9+
}.merge(attributes)
10+
11+
super(super_attributes)
12+
end
13+
end

lib/msf/core/modules/namespace.rb

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ module Msf::Modules::Namespace
1010
# @return [nil] if such as class is not defined.
1111
def metasploit_class
1212
metasploit_class = nil
13-
# don't search ancestors for the metasploit_class
14-
#inherit = false
1513

1614
::Msf::Framework::Major.downto(1) do |major|
1715
# Since we really only care about the deepest namespace, we don't
@@ -29,6 +27,19 @@ def metasploit_class
2927
metasploit_class
3028
end
3129

30+
def metasploit_class!(module_path, module_reference_name)
31+
metasploit_class = self.metasploit_class
32+
33+
unless metasploit_class
34+
raise Msf::Modules::MetasploitClassCompatibilityError.new(
35+
:module_path => module_path,
36+
:module_reference_name => module_reference_name
37+
)
38+
end
39+
40+
metasploit_class
41+
end
42+
3243
# Raises an error unless {Msf::Framework::VersionCore} and {Msf::Framework::VersionAPI} meet the minimum required
3344
# versions defined in RequiredVersions in the module content.
3445
#

lib/msf/core/modules/version_compatibility_error.rb

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,43 @@
1+
require 'msf/core/modules/error'
2+
13
# Error raised by {Msf::Modules::Namespace#version_compatible!} on {Msf::Modules::Loader::Base#create_namespace_module}
24
# if the API or Core version does not meet the minimum requirements defined in the RequiredVersions constant in the
35
# {Msf::Modules::Loader::Base#read_module_content module content}.
4-
class Msf::Modules::VersionCompatibilityError < StandardError
6+
class Msf::Modules::VersionCompatibilityError < Msf::Modules::Error
57
# @param [Hash{Symbol => Float}] attributes
68
# @option attributes [Float] :minimum_api_version The minimum {Msf::Framework::VersionAPI} as defined in
79
# RequiredVersions.
810
# @option attributes [Float] :minimum_core_version The minimum {Msf::Framework::VersionCore} as defined in
911
# RequiredVersions.
1012
def initialize(attributes={})
11-
@module_path = attributes[:module_path]
12-
@module_reference_name = attributes[:module_reference_name]
1313
@minimum_api_version = attributes[:minimum_api_version]
1414
@minimum_core_version = attributes[:minimum_core_version]
1515

16-
super("Failed to reload module (#{module_reference_name} from #{module_path}) due to version check " \
17-
"(requires API:#{minimum_api_version} Core:#{minimum_core_version})")
16+
message_parts = []
17+
message_parts << 'version check'
18+
19+
if minimum_api_version or minimum_core_version
20+
clause_parts = []
21+
22+
if minimum_api_version
23+
clause_parts << "API >= #{minimum_api_version}"
24+
end
25+
26+
if minimum_core_version
27+
clause_parts << "Core >= #{minimum_core_version}"
28+
end
29+
30+
clause = clause_parts.join(' and ')
31+
message_parts << "(requires #{clause})"
32+
end
33+
34+
causal_message = message_parts.join(' ')
35+
36+
super_attributes = {
37+
:causal_message => causal_message
38+
}.merge(attributes)
39+
40+
super(super_attributes)
1841
end
1942

2043
# @return [Float] The minimum value of {Msf::Framework::VersionAPI} for the module to be compatible.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
require 'spec_helper'
2+
3+
describe Msf::Modules::Error do
4+
context 'instance methods' do
5+
context '#initialize' do
6+
include_context 'Msf::Modules::Error attributes'
7+
8+
context 'with :causal_message' do
9+
subject do
10+
described_class.new(:causal_message => causal_message)
11+
end
12+
13+
it 'should include causal_message in error' do
14+
subject.to_s.should == "Failed to load module due to #{causal_message}"
15+
end
16+
end
17+
18+
context 'with :causal_message and :module_path' do
19+
subject do
20+
described_class.new(
21+
:causal_message => causal_message,
22+
:module_path => module_path
23+
)
24+
end
25+
26+
it 'should include causal_message and module_path in error' do
27+
subject.to_s.should == "Failed to load module (from #{module_path}) due to #{causal_message}"
28+
end
29+
end
30+
31+
context 'with :causal_message and :module_reference_name' do
32+
subject do
33+
described_class.new(
34+
:causal_message => causal_message,
35+
:module_reference_name => module_reference_name
36+
)
37+
end
38+
39+
it 'should include causal_message and module_reference_name in error' do
40+
subject.to_s.should == "Failed to load module (#{module_reference_name}) due to #{causal_message}"
41+
end
42+
end
43+
44+
context 'with :causal_message, :module_path, and :module_reference_nam' do
45+
subject do
46+
described_class.new(
47+
:causal_message => causal_message,
48+
:module_path => module_path,
49+
:module_reference_name => module_reference_name
50+
)
51+
end
52+
53+
it 'should include causal_message, module_path, and module_reference_name in error' do
54+
subject.to_s.should == "Failed to load module (#{module_reference_name} from #{module_path}) due to #{causal_message}"
55+
end
56+
end
57+
58+
context 'with :module_path' do
59+
subject do
60+
described_class.new(:module_path => module_path)
61+
end
62+
63+
it 'should use :module_path for module_path' do
64+
subject.module_path.should == module_path
65+
end
66+
67+
it 'should include module_path in error' do
68+
subject.to_s.should == "Failed to load module (from #{module_path})"
69+
end
70+
end
71+
72+
context 'with :module_path and :module_reference_name' do
73+
subject do
74+
described_class.new(
75+
:module_path => module_path,
76+
:module_reference_name => module_reference_name
77+
)
78+
end
79+
80+
it 'should include module_path and module_reference_name in error' do
81+
subject.to_s.should == "Failed to load module (#{module_reference_name} from #{module_path})"
82+
end
83+
end
84+
85+
context 'with :module_reference_name' do
86+
subject do
87+
described_class.new(:module_reference_name => module_reference_name)
88+
end
89+
90+
it 'should use :module_reference_name for module_reference_name' do
91+
subject.module_reference_name.should == module_reference_name
92+
end
93+
94+
it 'should include module_reference_name in error' do
95+
subject.to_s.should == "Failed to load module (#{module_reference_name})"
96+
end
97+
end
98+
99+
end
100+
end
101+
end

0 commit comments

Comments
 (0)