Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always use module cache for searching #13998

Merged

Conversation

adfoster-r7
Copy link
Contributor

@adfoster-r7 adfoster-r7 commented Aug 14, 2020

This pull request updates the slow module search to use the module cache.

Verification

1 - Console search

Open the console, and ensure that normal search functionality works as expected:

search monkey
search author:egypt arch:x64
search psexec author:egypt arch:x64

2 - rpcd

Create an instance of msfrpcd in one tab:

bundle exec ruby ./msfrpcd -P foo -f

In another create the client:

bundle exec ./ruby msfrpc -P foo -a 127.0.0.1

Ensure search works as expected, and aligns with the existing msfconsole search functionality:

rpc.call("module.search", "monkey")
rpc.call("module.search", "author:egypt arch:x64")
rpc.call("module.search", "psexec author:egypt arch:x64")

3 - json web service

The existing webservice should not be impacted by this code change, but for a sanity check - run the service with:

bundle exec thin --rackup msf-json-rpc.ru --address localhost --port 8081 --environment production --tag msf-json-rpc start

Run some search queries:

curl --request POST --url http://localhost:8081/api/v1/json-rpc --header 'content-type: application/json' --data '{ "jsonrpc": "2.0", "method": "module.search", "id": 1, "params": ["monkey"] }'
curl --request POST --url http://localhost:8081/api/v1/json-rpc --header 'content-type: application/json' --data '{ "jsonrpc": "2.0", "method": "module.search", "id": 1, "params": ["author:egypt arch:x64"] }'
curl --request POST --url http://localhost:8081/api/v1/json-rpc --header 'content-type: application/json' --data '{ "jsonrpc": "2.0", "method": "module.search", "id": 1, "params": ["psexec author:egypt arch:x64"] }'

@@ -57,7 +57,6 @@ class Module
include Msf::Module::Options
include Msf::Module::Privileged
include Msf::Module::Ranking
include Msf::Module::Search
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this is dead, and was only being used by tests

# {"platform"=>[["android"], []]} will match modules targeting the android platform
# {"platform"=>[[], ["android"]]} will exclude modules targeting the android platform
#
def self.parse_search_string(search_string)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the existing parse string functionality lifted from command_dispatcher/modules.rb

describe '#search_filter' do
require 'spec_helper'

RSpec.describe Msf::Modules::Metadata::Search do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided to port over the existing slow search tests and I've tried to keep as close to the original specification as I could, to ensure there was no unintentional regressions introduced. In a future PR I'll most likely expand/change these tests.

it "should reject a query containing '-#{query}'" do
expect(subject.search_filter("-#{query}")).to be_truthy
it "should reject an inverse query containing of '#{query}'" do
expect(find(inverse_query_terms(query))).to be_empty
Copy link
Contributor Author

@adfoster-r7 adfoster-r7 Aug 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new search cache has stricter validation, this test was previously producing invalid query strings which aren't supported by the search mechanism such as:

-os:osx

For the scope of these tests, the new inverse_query_terms just needs to support the translation to negating terms:

os:-osx

end

unless opts.has_key?(:test_inverse) and not opts[:test_inverse]
it "should accept a query containing '-#{query}'" do
expect(subject.search_filter("-#{query}")).to be_truthy # what? why?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expect(subject.search_filter("-#{query}")).to be_truthy # what? why?

I've updated this to verify the exact module that's been returned, rather than using be_truthy

@@ -72,65 +107,63 @@
end

context 'on a module with the author "joev"' do
let(:opts) { ({ 'Author' => ['joev'] }) }
let(:opts) { ({ 'author' => ['joev'] }) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously these opts were used to hydrate a module, now they're hydrating the search cache - as a result I've had to update some of the keys.

#
# Searches the module metadata using the passed hash of search params
#
def find(params, fields={})
raise ArgumentError if params.empty? || VALID_PARAMS.none? { |k| params.key?(k) }
raise ArgumentError if params.any? && VALID_PARAMS.none? { |k| params.key?(k) }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An empty search query will now result in all modules being returned, which aligns with the previous search functionality offered by framework's slow search / the old rpc search - #8606

The console never went down this path, as there's a guard clause in the dispatcher:

if args.empty?
if @module_search_results.empty?
cmd_search_help
return false
end
cached = true
end

So this feels like a safe/valid change to make 🤞

@adfoster-r7 adfoster-r7 force-pushed the always-use-module-cache-for-searching branch from ad020b2 to baa33df Compare August 14, 2020 18:15
# {"platform"=>[[], ["android"]]} will exclude modules targeting the android platform
#
def self.parse_search_string(search_string)
search_string += ' '
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure why you're adding a space to the end of this only to split on spaces on the next line, it doesn't make a difference right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was copied from the original implementation, I noticed this problem on the RPC service, with the input:

author:egypt arch:x64

This parsing method gives the following result:

=> {"author"=>[["egypt arch:x64"], []]}

With the extra space appended, it gives the expected result of:

=> {"author"=>[["egypt"], []], "arch"=>[["x64"], []]}

I'll add a unit test to make sure this scenario is caught correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests added. The tests were red without this line:

Failure/Error: it { expect(described_class.parse_search_string("author:egypt arch:x64")).to eq({"author"=>[["egypt"], []], "arch"=>[["x64"], []]}) }
     
       expected: {"arch"=>[["x64"], []], "author"=>[["egypt"], []]}
            got: {"author"=>[["egypt arch:x64"], []]}
     
       (compared using ==)
     
       Diff:
       @@ -1,3 +1,2 @@
       -"arch" => [["x64"], []],
       -"author" => [["egypt"], []],
       +"author" => [["egypt arch:x64"], []],

And now it's green with the line added back in 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def parse_search_string(search_string)

isn't this where you got the code from? unless I'm missing something no space is being added to the search term

Copy link
Contributor Author

@adfoster-r7 adfoster-r7 Aug 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of the space is from the slow search that I'm unifying:

def search_filter(search_string)
return false if not search_string
search_string += " "

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah gotcha, that one has a guard clause, does that need added in here too? or are we stopping nil being passed in from elsewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's technically possible to provide nil via a message pack rpc call. I've updated this to handle nil, and added tests for an entirely empty string too. A nil search string will now result in empty search params, and thus return all modules.

@adfoster-r7 adfoster-r7 force-pushed the always-use-module-cache-for-searching branch 2 times, most recently from 341ca70 to 0491d34 Compare August 19, 2020 08:35
@adfoster-r7 adfoster-r7 force-pushed the always-use-module-cache-for-searching branch from 0491d34 to f8523cb Compare August 19, 2020 08:37
@dwelch-r7 dwelch-r7 merged commit 3d1eba2 into rapid7:master Aug 19, 2020
@adfoster-r7 adfoster-r7 deleted the always-use-module-cache-for-searching branch August 19, 2020 14:00
@adfoster-r7 adfoster-r7 added rn-enhancement release notes enhancement and removed rn-no-release-notes no release notes labels Aug 19, 2020
@adfoster-r7
Copy link
Contributor Author

adfoster-r7 commented Aug 19, 2020

Release notes

Greatly improved the performance of Metasploit Framework's module.search rpc call by searching the module cache instead of Framework's previous slow search functionality.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement rn-enhancement release notes enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants