diff --git a/lib/msf/base/serializer/readable_text.rb b/lib/msf/base/serializer/readable_text.rb index ee5a108fd91e7..26caf1b2f3a37 100644 --- a/lib/msf/base/serializer/readable_text.rb +++ b/lib/msf/base/serializer/readable_text.rb @@ -834,7 +834,7 @@ def self.dump_datastore(name, ds, indent = DefaultIndent, col = DefaultColumnWra def self.dump_sessions(framework, opts={}) output = "" verbose = opts[:verbose] || false - session_ids = opts[:session_ids] || nil + sessions = opts[:sessions] || framework.sessions show_active = opts[:show_active] || false show_inactive = opts[:show_inactive] || false # if show_active and show_inactive are false the caller didn't @@ -861,12 +861,10 @@ def self.dump_sessions(framework, opts={}) 'Columns' => columns, 'Indent' => indent) - framework.sessions.each { |k| - next unless session_ids.nil? || session_ids.include?(k[0]) - session = k[1] + sessions.each do |session_id, session| row = create_msf_session_row(session, show_extended) tbl << row - } + end output << (tbl.rows.count > 0 ? tbl.to_s : "#{tbl.header_to_s}No active sessions.\n") end @@ -987,14 +985,9 @@ def self.dump_sessions_verbose(framework, opts={}) return out end - framework.sessions.each_sorted do |k| - session = framework.sessions[k] + sessions = opts[:sessions] || framework.sessions - if opts[:session_ids] - unless opts[:session_ids].include? session.sid - next - end - end + sessions.each do |session_id, session| sess_info = session.info.to_s sess_id = session.sid.to_s sess_name = session.sname.to_s diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index 34e96c1a283cb..b781dd80ff442 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -114,12 +114,14 @@ class Core ["-g", "--global"] => [ false, "Operate on global datastore variables"] ) - VALID_PARAMS = + VALID_SESSION_SEARCH_PARAMS = %w[ last_checkin session_id session_type ] + + private_constant :VALID_SESSION_SEARCH_PARAMS # Returns the list of commands supported by this command dispatcher def commands @@ -1431,7 +1433,6 @@ def cmd_sessions(*args) begin method = nil quiet = false - search = false show_active = false show_inactive = false show_extended = false @@ -1536,6 +1537,14 @@ def cmd_sessions(*args) print_warning("Database not connected; list of inactive sessions unavailable") end + if search_term + matching_sessions = get_matching_sessions(search_term) + if matching_sessions.empty? + print_status("No matching sessions.") + return + end + end + last_known_timeout = nil # Now, perform the actual method @@ -1545,13 +1554,7 @@ def cmd_sessions(*args) print_error("No command specified!") return false end - if search_term - matching_sessions = get_matching_sessions(search_term) - if matching_sessions.empty? - print_error("No matching sessions.") - return - end - end + cmds.each do |cmd| if sid sessions = session_list @@ -1666,25 +1669,16 @@ def cmd_sessions(*args) end end when 'killall' - if search_term - matching_sessions = get_matching_sessions(search_term) - if matching_sessions.empty? - print_status("No matching sessions.") - return - end + if matching_sessions print_status("Killing matching sessions...") print_line - print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, session_ids: matching_sessions.keys())) + print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, sessions: matching_sessions)) print_line else matching_sessions = framework.sessions print_status("Killing all sessions...") end matching_sessions.each do |session_id, session| - unless matching_sessions.nil? || matching_sessions.include?(session_id) - next - end - if session if session.respond_to?(:response_timeout) last_known_timeout = session.response_timeout @@ -1786,18 +1780,8 @@ def cmd_sessions(*args) end end when 'list', 'list_inactive', nil - if search_term - matching_sessions = get_matching_sessions(search_term) - if matching_sessions.empty? - print_error("No matching sessions.") - return - end - end - if matching_sessions - session_ids = matching_sessions.keys() - end print_line - print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, session_ids: session_ids)) + print(Serializer::ReadableText.dump_sessions(framework, show_active: show_active, show_inactive: show_inactive, show_extended: show_extended, verbose: verbose, sessions: matching_sessions)) print_line when 'name' if session_name.blank? @@ -1839,40 +1823,84 @@ def cmd_sessions(*args) def get_matching_sessions(search_term) matching_sessions = {} terms = search_term.split + id_searches = [] + type_searches = [] + checkin_searches = [] terms.each do |term| - matches = filter_sessions_by_search(term) - matches.each do |session_id, session| - matching_sessions.store(session_id, session) + case term.split(":").first + when "session_id" + id_searches << term + when "session_type" + type_searches << term + when "last_checkin" + checkin_searches << term + else + raise ArgumentError, "Please provide valid search term. Given: #{term.split(":").first}" end end + searches = [] + unless id_searches.empty? + id_matches = {} + id_searches.each do |term| + matches = filter_sessions_by_search(term) + id_matches = id_matches.merge(matches) + end + searches << id_matches + end + unless type_searches.empty? + type_matches = {} + type_searches.each do |term| + matches = filter_sessions_by_search(term) + type_matches = type_matches.merge(matches) + end + searches << type_matches + end + unless checkin_searches.empty? + checkin_matches = {} + first_loop = true + checkin_searches.each do |term| + matches = filter_sessions_by_search(term) + if first_loop + checkin_matches = matches + else + checkin_matches = checkin_matches.select{ |session_id, session| matches[session_id] == session } + end + end + searches << checkin_matches + end + if searches + matching_sessions = searches.first + searches[1..].each do |result_set| + matching_sessions = matching_sessions.select{| session_id, session| result_set[session_id] == session} + end + else + raise ArgumentError, "Please provide a valid search query." + end matching_sessions end def filter_sessions_by_search(search_term) matching_sessions = {} - + field = search_term.split(":")[0] framework.sessions.each do |session_id, session| - case search_term.split(":")[0] + case field when "last_checkin" if session.respond_to?(:last_checkin) && session.last_checkin && evaluate_search_criteria(session, search_term) - matching_sessions.store(session_id, session) + matching_sessions[session_id] = session end - when "session_type" - matching_sessions.store(session_id, session) if evaluate_search_criteria(session, search_term) - when "session_id" - matching_sessions.store(session_id, session) if evaluate_search_criteria(session, search_term) + when "session_type", "session_id" + matching_sessions[session_id] = session if evaluate_search_criteria(session, search_term) + else + raise ArgumentError, "Unrecognized search term: #{field}" end end matching_sessions end def evaluate_search_criteria(session, search_term) - parts = search_term.split(":") + field, operator, value = search_term.split(":") - field = parts[0] - operator = parts[1] - value = parts[2] if parts[2] - return false unless VALID_PARAMS.include? parts[0] + raise ArgumentError, "Unrecognized search term: #{field}" unless VALID_SESSION_SEARCH_PARAMS.include? field case field when "last_checkin" @@ -1880,38 +1908,38 @@ def evaluate_search_criteria(session, search_term) threshold_time = Time.now - parse_duration(value) case operator when "before" - return checkin_time > threshold_time - when "after" return checkin_time < threshold_time + when "after" + return checkin_time > threshold_time else return false end when "session_id" return session.sid.to_s == operator when "session_type" - return session.type == operator + return session.type.casecmp?(operator) end end def parse_duration(duration) total_time = 0 - time_tokens = duration.scan(/\d+/).zip(duration.scan(/[a-zA-Z]+/)) - time_tokens.each do |pair| - raise "Please specify both time units and amounts" if pair[1].nil? - case pair[1] + time_tokens = duration.scan(/(?:\d+\.?\d*|\.\d+)/).zip(duration.scan(/[a-zA-Z]+/)) + time_tokens.each do |value, unit| + raise ArgumentError, "Please specify both time units and amounts" if unit.nil? || value.nil? + case unit when "d" - total_time = total_time + pair[0].to_i * 86400 + total_time += value.to_f * 86400 when "h" - total_time = total_time + pair[0].to_i * 3600 + total_time += value.to_f * 3600 when "m" - total_time = total_time + pair[0].to_i * 60 + total_time += value.to_f * 60 when "s" - total_time = total_time + pair[0].to_i + total_time += value.to_f else - raise "Unrecognized time format" + raise ArgumentError, "Unrecognized time format: #{value}" end end - return total_time + total_time.to_i end # diff --git a/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb b/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb index 4ab6e162ffaea..3e13e23a4b19c 100644 --- a/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb +++ b/spec/lib/msf/ui/console/command_dispatcher/core_spec.rb @@ -398,71 +398,96 @@ def set_tabs_test(option) end end + #TODO: restructure hierarchy, test time parsing, and add more coverage describe "#cmd_sessions" do before(:each) do allow(driver).to receive(:active_session=) - + allow(framework).to receive(:sessions).and_return(sessions) end context "with no sessions" do + let(:sessions) {[]} it "should show an empty table" do core.cmd_sessions - expect(@output).to eq([ - 'Active sessions', - '===============', - '', - 'No active sessions.' - ]) + expect(@output.join("\n")).to match_table <<~TABLE + Active sessions + =============== + + No active sessions. + TABLE end end context "filtering with search results" do - before do - allow(framework).to receive(:sessions).and_return(sessions) - allow(double "Session").to receive(:kill) - end + #TODO: use timecop to lock time in place let(:sessions) do { - 1 => double('Session', last_checkin: Time.now, type: 'meterpreter', sid:1, sname: "sesh1", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel"), - 2 => double('Session', last_checkin: (Time.now - 90), type: 'meterpreter', sid:2, sname: "sesh2", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel"), - 3 => double('Session', last_checkin: Time.now, type: 'java', sid:3, sname: "sesh3", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel") + 1 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'cmd_shell', sid:1, sname: "sesh1", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel"), + 2 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: (Time.now - 90), type: 'meterpreter', sid:2, sname: "sesh2", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel"), + 3 => instance_double(::Msf::Sessions::Meterpreter_x64_Win, last_checkin: Time.now, type: 'java', sid:3, sname: "sesh3", info: "info", session_host: "127.0.0.1", tunnel_to_s: "tunnel") } end it "filters by session_id" do - core.cmd_sessions("--search", "session_id:2") + core.cmd_sessions("--search", "session_id:2 session_id:1") expect(@output.join("\n")).to match_table <<~TABLE - Active sessions - =============== - - Id Name Type Information Connection - -- ---- ---- ----------- ---------- - 2 sesh2 meterpreter info tunnel (127.0.0.1) + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 1 sesh1 cmd_shell info tunnel (127.0.0.1) + 2 sesh2 meterpreter info tunnel (127.0.0.1) TABLE end - it "filters by last checkin" do - core.cmd_sessions("--search", "last_checkin:after:10s") + it "filters by multiple session types" do + core.cmd_sessions("--search", "session_type:meterpreter session_type:java") expect(@output.join("\n")).to match_table <<~TABLE - Active sessions - =============== - - Id Name Type Information Connection - -- ---- ---- ----------- ---------- - 2 sesh2 meterpreter info tunnel (127.0.0.1) + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 2 sesh2 meterpreter info tunnel (127.0.0.1) + 3 sesh3 java info tunnel (127.0.0.1) + TABLE + end + + it "filters by last checkin using fractions of a second" do + core.cmd_sessions("--search", "last_checkin:before:4.5s") + expect(@output.join("\n")).to match_table <<~TABLE + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 2 sesh2 meterpreter info tunnel (127.0.0.1) + TABLE + end + + it "filters by last checkin using fractions of a minute" do + core.cmd_sessions("--search", "last_checkin:before:0.5m1s") + expect(@output.join("\n")).to match_table <<~TABLE + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 2 sesh2 meterpreter info tunnel (127.0.0.1) TABLE end it "filters by session type" do core.cmd_sessions("--search", "session_type:java") expect(@output.join("\n")).to match_table <<~TABLE - Active sessions - =============== - - Id Name Type Information Connection - -- ---- ---- ----------- ---------- - 3 sesh3 java info tunnel (127.0.0.1) + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 3 sesh3 java info tunnel (127.0.0.1) TABLE end @@ -477,14 +502,14 @@ def set_tabs_test(option) expect(sessions[2]).to receive(:kill) core.cmd_sessions("--search", "session_id:2 session_id:3", "-K") expect(@output.join("\n")).to match_table <<~TABLE - Killing matching sessions... - Active sessions - =============== - - Id Name Type Information Connection - -- ---- ---- ----------- ---------- - 2 sesh2 meterpreter info tunnel (127.0.0.1) - 3 sesh3 java info tunnel (127.0.0.1) + Killing matching sessions... + Active sessions + =============== + + Id Name Type Information Connection + -- ---- ---- ----------- ---------- + 2 sesh2 meterpreter info tunnel (127.0.0.1) + 3 sesh3 java info tunnel (127.0.0.1) TABLE end end