Msf::DBManager#report_session specs

[#47979793]
This commit is contained in:
Luke Imhoff
2013-04-19 10:11:33 -05:00
parent 2c681005c0
commit e5befb7094
5 changed files with 760 additions and 6 deletions
+2
View File
@@ -48,4 +48,6 @@ group :test do
# code coverage for tests
# any version newer than 0.5.4 gives an Encoding error when trying to read the source files.
gem 'simplecov', '0.5.4', :require => false
# Manipulate Time.now in specs
gem 'timecop'
end
+2
View File
@@ -52,6 +52,7 @@ GEM
multi_json (~> 1.0.3)
simplecov-html (~> 0.5.3)
simplecov-html (0.5.3)
timecop (0.6.1)
tzinfo (0.3.37)
yard (0.8.5.2)
@@ -75,4 +76,5 @@ DEPENDENCIES
rspec (>= 2.12)
shoulda-matchers
simplecov (= 0.5.4)
timecop
yard
+61 -6
View File
@@ -645,12 +645,69 @@ class DBManager
}
end
# Record a new session in the database
# @note The Mdm::Session#desc will be truncated to 255 characters.
# @todo https://www.pivotaltracker.com/story/show/48249739
#
# opts MUST contain either
# +:session+:: the Msf::Session object we are reporting
# +:host+:: the Host object we are reporting a session on.
# @overload report_session(opts)
# Creates an Mdm::Session from Msf::Session. If +via_exploit+ is set on the
# +session+, then an Mdm::Vuln and Mdm::ExploitAttempt is created for the
# session's host. The Mdm::Host for the +session_host+ is created using
# The session.session_host, +session.arch+ (if +session+ responds to arch),
# and the workspace derived from opts or the +session+. The Mdm::Session is
# assumed to be +last_seen+ and +opened_at+ at the time report_session is
# called. +session.exploit_datastore['ParentModule']+ is used for the
# Mdm::Session#via_exploit if +session.via_exploit+ is
# 'exploit/multi/handler'.
#
# @param opts [Hash{Symbol => Object}] options
# @option opt [Msf::Session, #datastore, #platform, #type, #via_exploit, #via_payload] :session
# The in-memory session to persist to the database.
# @option opts [Mdm::Workspace] :workspace The workspace for in which the
# :session host is contained. Also used as the workspace for the
# Mdm::ExploitAttempt and Mdm::Vuln. Defaults to Mdm::Worksapce with
# Mdm::Workspace#name equal to +session.workspace+.
# @return [nil] if {Msf::DBManager#active} is +false+.
# @return [Mdm::Session] if session is saved
# @raise [ArgumentError] if :session is not an {Msf::Session}.
# @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be
# saved, in which case, the Mdm::ExploitAttempt and Mdm::Vuln will not be
# created, but the Mdm::Host will have been. (There is no transaction
# to rollback the Mdm::Host creation.)
# @see #find_or_create_host
# @see #normalize_host
# @see #report_exploit_success
# @see #report_vuln
#
# @overload report_session(opts)
# Creates an Mdm::Session from Mdm::Host.
#
# @param opts [Hash{Symbol => Object}] options
# @option opts [DateTime, Time] :closed_at The date and time the sesion was
# closed.
# @option opts [String] :close_reason Reason the session was closed.
# @option opts [Hash] :datastore {Msf::DataStore#to_h}.
# @option opts [String] :desc Session description. Will be truncated to 255
# characters.
# @option opts [Mdm::Host] :host The host on which the session was opened.
# @option opts [DateTime, Time] :last_seen The last date and time the
# session was seen to be open. Defaults to :closed_at's value.
# @option opts [DateTime, Time] :opened_at The date and time that the
# session was opened.
# @option opts [String] :platform The platform of the host.
# @option opts [Array] :routes ([]) The routes through the session for
# pivoting.
# @option opts [String] :stype Session type.
# @option opts [String] :via_exploit The {Msf::Module#fullname} of the
# exploit that was used to open the session.
# @option option [String] :via_payload the {MSf::Module#fullname} of the
# payload sent to the host when the exploit was successful.
# @return [nil] if {Msf::DBManager#active} is +false+.
# @return [Mdm::Session] if session is saved.
# @raise [ArgumentError] if :host is not an Mdm::Host.
# @raise [ActiveRecord::RecordInvalid] if session is invalid and cannot be
# saved.
#
# @raise ArgumentError if :host and :session is +nil+
def report_session(opts)
return if not active
::ActiveRecord::Base.connection_pool.with_connection {
@@ -719,8 +776,6 @@ class DBManager
# If this is a live session, we know the host is vulnerable to something.
if opts[:session] and session.via_exploit
return unless host
mod = framework.modules.create(session.via_exploit)
if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule']
+34
View File
@@ -0,0 +1,34 @@
FactoryGirl.define do
factory :mdm_route, :class => Mdm::Route do
netmask { generate :mdm_route_netmask }
subnet { generate :mdm_route_subnet }
#
# Associations
#
association :session, :factory => :mdm_session
end
sequence :mdm_route_netmask do |n|
bits = 32
bitmask = n % bits
[ (~((2 ** (bits - bitmask)) - 1)) & 0xffffffff ].pack('N').unpack('CCCC').join('.')
bits = 32
shift = n % bits
mask_range = 2 ** bits
full_mask = mask_range - 1
integer_netmask = (full_mask << shift)
formatted_netmask = [integer_netmask].pack('N').unpack('CCCC').join('.')
formatted_netmask
end
sequence :mdm_route_subnet do |n|
class_c_network = n % 255
"192.168.#{class_c_network}.0"
end
end
+661
View File
@@ -19,4 +19,665 @@ describe Msf::DBManager do
end
it_should_behave_like 'Msf::DBManager::ImportMsfXml'
context '#report_session' do
include_context 'DatabaseCleaner'
let(:options) do
{}
end
subject(:report_session) do
db_manager.report_session(options)
end
before(:each) do
configurations = Metasploit::Framework::Database.configurations
spec = configurations[Metasploit::Framework.env]
# Need to connect or ActiveRecord::Base.connection_pool will raise an
# error.
db_manager.connect(spec)
db_manager.stub(:active => active)
end
context 'with active' do
let(:active) do
true
end
it 'should create connection' do
# 1st time from with_established_connection
# 2nd time from report_session
ActiveRecord::Base.connection_pool.should_receive(:with_connection).exactly(2).times
report_session
end
context 'with :session' do
before(:each) do
options[:session] = session
end
context 'with Msf::Session' do
let(:exploit_datastore) do
Msf::ModuleDataStore.new(module_instance).tap do |datastore|
datastore['ParentModule'] = parent_module_fullname
remote_port = rand(2 ** 16 - 1)
datastore['RPORT'] = remote_port
end
end
let(:host) do
FactoryGirl.create(:mdm_host, :workspace => session_workspace)
end
let(:module_instance) do
name = 'multi/handler'
mock(
'Msf::Module',
:fullname => "exploit/#{name}",
:framework => framework,
:name => name
)
end
let(:options_workspace) do
FactoryGirl.create(:mdm_workspace)
end
let(:parent_module_fullname) do
"exploit/#{parent_module_name}"
end
let(:parent_module_name) do
'windows/smb/ms08_067_netapi'
end
let(:parent_path) do
Metasploit::Framework.root.join('modules').to_path
end
let(:session) do
session_class.new.tap do |session|
session.exploit_datastore = exploit_datastore
session.info = 'Info'
session.platform = 'Platform'
session.session_host = host.address
session.sid = rand(100)
session.type = 'Session Type'
session.via_exploit = 'exploit/multi/handler'
session.via_payload = 'payload/single/windows/metsvc_bind_tcp'
session.workspace = session_workspace.name
end
end
let(:session_class) do
Class.new do
include Msf::Session
attr_accessor :datastore
attr_accessor :platform
attr_accessor :type
attr_accessor :via_exploit
attr_accessor :via_payload
end
end
let(:session_workspace) do
FactoryGirl.create(:mdm_workspace)
end
before(:each) do
reference_name = 'multi/handler'
path = File.join(parent_path, 'exploits', reference_name)
# fake cache data for exploit/multi/handler so it can be loaded
framework.modules.send(
:module_info_by_path=,
{
path =>
{
:parent_path => parent_path,
:reference_name => reference_name,
:type => 'exploit',
}
}
)
FactoryGirl.create(
:mdm_module_detail,
:fullname => parent_module_fullname,
:name => parent_module_name
)
end
context 'with :workspace' do
before(:each) do
options[:workspace] = options_workspace
end
it 'should not find workspace from session' do
db_manager.should_not_receive(:find_workspace)
report_session
end
end
context 'without :workspace' do
it 'should find workspace from session' do
db_manager.should_receive(:find_workspace).with(session.workspace).and_call_original
report_session
end
it 'should pass session.workspace to #find_or_create_host' do
db_manager.should_receive(:find_or_create_host).with(
hash_including(
:workspace => session_workspace
)
).and_return(host)
report_session
end
end
context 'with workspace from either :workspace or session' do
it 'should pass normalized host from session as :host to #find_or_create_host' do
normalized_host = mock('Normalized Host')
db_manager.stub(:normalize_host).with(session).and_return(normalized_host)
# stub report_vuln so its use of find_or_create_host and normalize_host doesn't interfere.
db_manager.stub(:report_vuln)
db_manager.should_receive(:find_or_create_host).with(
hash_including(
:host => normalized_host
)
).and_return(host)
report_session
end
context 'with session responds to arch' do
let(:arch) do
'Arch'
end
before(:each) do
session.stub(:arch => arch)
end
it 'should pass :arch to #find_or_create_host' do
db_manager.should_receive(:find_or_create_host).with(
hash_including(
:arch => arch
)
).and_call_original
report_session
end
end
context 'without session responds to arch' do
it 'should not pass :arch to #find_or_create_host' do
db_manager.should_receive(:find_or_create_host).with(
hash_excluding(
:arch
)
).and_call_original
report_session
end
end
it 'should create an Mdm::Session' do
expect {
report_session
}.to change(Mdm::Session, :count).by(1)
end
it { should be_an Mdm::Session }
it 'should set session.db_record to created Mdm::Session' do
mdm_session = report_session
session.db_record.should == mdm_session
end
context 'with session.via_exploit' do
it 'should create session.via_exploit module' do
framework.modules.should_receive(:create).with(session.via_exploit).and_call_original
report_session
end
it 'should create Mdm::Vuln' do
expect {
report_session
}.to change(Mdm::Vuln, :count).by(1)
end
context 'created Mdm::Vuln' do
let(:mdm_session) do
Mdm::Session.last
end
let(:rport) do
nil
end
before(:each) do
Timecop.freeze
session.exploit_datastore['RPORT'] = rport
report_session
end
after(:each) do
Timecop.return
end
subject(:vuln) do
Mdm::Vuln.last
end
its(:host) { should == Mdm::Host.last }
its(:refs) { should == [] }
its(:exploited_at) { should == Time.now.utc }
context "with session.via_exploit 'exploit/multi/handler'" do
context "with session.exploit_datastore['ParentModule']" do
its(:info) { should == "Exploited by #{parent_module_fullname} to create Session #{mdm_session.id}" }
its(:name) { should == parent_module_name }
end
end
context "without session.via_exploit 'exploit/multi/handler'" do
let(:reference_name) do
'windows/smb/ms08_067_netapi'
end
before(:each) do
path = File.join(
parent_path,
'exploits',
"#{reference_name}.rb"
)
type = 'exploit'
# fake cache data for ParentModule so it can be loaded
framework.modules.send(
:module_info_by_path=,
{
path =>
{
:parent_path => parent_path,
:reference_name => reference_name,
:type => type,
}
}
)
session.via_exploit = "#{type}/#{reference_name}"
end
its(:info) { should == "Exploited by #{session.via_exploit} to create Session #{mdm_session.id}"}
its(:name) { should == reference_name }
end
context 'with RPORT' do
let(:rport) do
# use service.port instead of having service use rport so
# that service is forced to exist before call to
# report_service, which happens right after using rport in
# outer context's before(:each)
service.port
end
let(:service) do
FactoryGirl.create(
:mdm_service,
:host => host
)
end
its(:service) { should == service }
end
context 'without RPORT' do
its(:service) { should be_nil }
end
end
context 'created Mdm::ExploitAttempt' do
let(:rport) do
nil
end
before(:each) do
Timecop.freeze
session.exploit_datastore['RPORT'] = rport
report_session
end
after(:each) do
Timecop.return
end
subject(:exploit_attempt) do
Mdm::ExploitAttempt.last
end
its(:attempted_at) { should == Time.now.utc }
# @todo https://www.pivotaltracker.com/story/show/48362615
its(:session_id) { should == Mdm::Session.last.id }
its(:exploited) { should == true }
# @todo https://www.pivotaltracker.com/story/show/48362615
its(:vuln_id) { should == Mdm::Vuln.last.id }
context "with session.via_exploit 'exploit/multi/handler'" do
context "with session.datastore['ParentModule']" do
its(:module) { should == parent_module_fullname }
end
end
context "without session.via_exploit 'exploit/multi/handler'" do
before(:each) do
session.via_exploit = parent_module_fullname
end
its(:module) { should == session.via_exploit }
end
end
end
context 'returned Mdm::Session' do
before(:each) do
Timecop.freeze
end
after(:each) do
Timecop.return
end
subject(:mdm_session) do
report_session
end
#
# Ensure session has attributes present so its on mdm_session are
# not just comparing nils.
#
it 'should have session.info present' do
session.info.should be_present
end
it 'should have session.sid present' do
session.sid.should be_present
end
it 'should have session.platform present' do
session.platform.should be_present
end
it 'should have session.type present' do
session.type.should be_present
end
it 'should have session.via_exploit present' do
session.via_exploit.should be_present
end
it 'should have session.via_payload present' do
session.via_exploit.should be_present
end
its(:datastore) { should == session.exploit_datastore.to_h }
its(:desc) { should == session.info }
its(:host_id) { should == Mdm::Host.last.id }
its(:last_seen) { should == Time.now.utc }
its(:local_id) { should == session.sid }
its(:opened_at) { should == Time.now.utc }
its(:platform) { should == session.platform }
its(:routes) { should == [] }
its(:stype) { should == session.type }
its(:via_payload) { should == session.via_payload }
context "with session.via_exploit 'exploit/multi/handler'" do
it "should have session.via_exploit of 'exploit/multi/handler'" do
session.via_exploit.should == 'exploit/multi/handler'
end
context "with session.exploit_datastore['ParentModule']" do
it "should have session.exploit_datastore['ParentModule']" do
session.exploit_datastore['ParentModule'].should_not be_nil
end
its(:via_exploit) { should == parent_module_fullname }
end
end
context "without session.via_exploit 'exploit/multi/handler'" do
before(:each) do
reference_name = 'windows/smb/ms08_067_netapi'
path = File.join(
parent_path,
'exploits',
"#{reference_name}.rb"
)
type = 'exploit'
# fake cache data for ParentModule so it can be loaded
framework.modules.send(
:module_info_by_path=,
{
path =>
{
:parent_path => parent_path,
:reference_name => reference_name,
:type => type,
}
}
)
session.via_exploit = "#{type}/#{reference_name}"
end
it "should not have session.via_exploit of 'exploit/multi/handler'" do
session.via_exploit.should_not == 'exploit/multi/handler'
end
its(:via_exploit) { should == session.via_exploit }
end
end
end
end
context 'without Msf::Session' do
let(:session) do
mock('Not a Msf::Session')
end
it 'should raise ArgumentError' do
expect {
report_session
}.to raise_error(ArgumentError, "Invalid :session, expected Msf::Session")
end
end
end
context 'without :session' do
context 'with :host' do
before(:each) do
options[:host] = host
end
context 'with Mdm::Host' do
let(:host) do
FactoryGirl.create(:mdm_host)
end
context 'created Mdm::Session' do
let(:closed_at) do
nil
end
let(:close_reason) do
'Closed because...'
end
let(:description) do
'Session Description'
end
let(:exploit_full_name) do
'exploit/windows/smb/ms08_067_netapi'
end
let(:last_seen) do
nil
end
let(:opened_at) do
Time.now.utc - 5.minutes
end
let(:payload_full_name) do
'payload/singles/windows/metsvc_reverse_tcp'
end
let(:platform) do
'Host Platform'
end
let(:routes) do
nil
end
let(:session_type) do
'Session Type'
end
before(:each) do
options[:closed_at] = closed_at
options[:close_reason] = close_reason
options[:desc] = description
options[:last_seen] = last_seen
options[:opened_at] = opened_at
options[:platform] = platform
options[:routes] = routes
options[:stype] = session_type
options[:via_payload] = payload_full_name
options[:via_exploit] = exploit_full_name
end
subject(:mdm_session) do
report_session
end
its(:close_reason) { should == close_reason }
its(:desc) { should == description }
its(:host) { should == host }
its(:platform) { should == platform }
its(:stype) { should == session_type }
its(:via_exploit) { should == exploit_full_name }
its(:via_payload) { should == payload_full_name }
context 'with :last_seen' do
let(:last_seen) do
opened_at
end
its(:last_seen) { should == last_seen }
end
context 'with :closed_at' do
let(:closed_at) do
opened_at + 1.minute
end
its(:closed_at) { should == closed_at }
end
context 'without :closed_at' do
its(:closed_at) { should == nil }
end
context 'without :last_seen' do
context 'with :closed_at' do
let(:closed_at) do
opened_at + 1.minute
end
its(:last_seen) { should == closed_at }
end
context 'without :closed_at' do
its(:last_seen) { should be_nil }
end
end
context 'with :routes' do
let(:routes) do
FactoryGirl.build_list(
:mdm_route,
1,
:session => nil
)
end
its(:routes) { should == routes }
end
context 'without :routes' do
its(:routes) { should == [] }
end
end
end
context 'without Mdm::Host' do
let(:host) do
'192.168.0.1'
end
it 'should raise ArgumentError' do
expect {
report_session
}.to raise_error(ArgumentError, "Invalid :host, expected Host object")
end
end
end
context 'without :host' do
it 'should raise ArgumentError' do
expect {
report_session
}.to raise_error(ArgumentError)
end
end
end
end
context 'without active' do
let(:active) do
false
end
it { should be_nil }
it 'should not create a connection' do
# 1st time for with_established_connection
ActiveRecord::Base.connection_pool.should_receive(:with_connection).once
report_session
end
end
end
end