moby.rb 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,119 @@
1
+ # lib/Moby/Configuration.rb
2
+ # Moby::Configuration
3
+
4
+ require 'mechanize'
5
+
6
+ class Moby
7
+ class Configuration
8
+ AVAILABLE_USER_AGENTS = (Mechanize::AGENT_ALIASES.keys - ['Mechanize']).freeze
9
+
10
+ attr_accessor(
11
+ :url,
12
+ :backend,
13
+ :browser,
14
+ :debug,
15
+ :verbose,
16
+ :form_name,
17
+ :form_number,
18
+ :username_field_name,
19
+ :username_field_number,
20
+ :password_field_name,
21
+ :password_field_number,
22
+ :username_hostname,
23
+ :username_is_email_address,
24
+ :username_prefilled,
25
+ :iterations,
26
+ :delay,
27
+ :word_list_path
28
+ )
29
+
30
+ attr_writer(
31
+ :user_agent,
32
+ )
33
+
34
+ def user_agent
35
+ @user_agent || random_user_agent
36
+ end
37
+
38
+ def using_form_name?
39
+ !@form_name.nil?
40
+ end
41
+
42
+ def using_username_field_name?
43
+ !@username_field_name.nil?
44
+ end
45
+
46
+ def using_password_field_name?
47
+ !@password_field_name.nil?
48
+ end
49
+
50
+ def mechanize?
51
+ @backend == :mechanize
52
+ end
53
+
54
+ def selenium?
55
+ @backend == :selenium
56
+ end
57
+
58
+ def username_is_email_address?
59
+ @username_is_email_address || @username_hostname || @username_field_name == 'email'
60
+ end
61
+
62
+ def username_prefilled?
63
+ @username_prefilled
64
+ end
65
+
66
+ def debug?
67
+ @debug
68
+ end
69
+
70
+ def verbose?
71
+ @verbose
72
+ end
73
+
74
+ def fixed_number_of_submissions?
75
+ !!@iterations
76
+ end
77
+
78
+ private
79
+
80
+ # @param url [String] The target URL
81
+ # @param backend [Symbol] :mechanize or :selenium (default: :mechanize)
82
+ # @param browser [Symbol] :chrome or :firefox (default: :chrome)
83
+
84
+ def initialize(url:, backend: :mechanize, browser: :chrome, **args)
85
+ @url = url
86
+ @backend = (backend || :mechanize).to_sym
87
+
88
+ # Browser configuration (for Selenium)
89
+ @browser = (browser || :chrome).to_sym
90
+ @user_agent = args[:user_agent]
91
+
92
+ # Form identification
93
+ @form_name = args[:form_name]
94
+ @form_number = (args[:form_number] || 0).to_i
95
+
96
+ # Field identification
97
+ @username_field_name = args[:username_field_name] || 'username'
98
+ @username_field_number = (args[:username_field_number] || 0).to_i
99
+ @password_field_name = args[:password_field_name] || 'password'
100
+ @password_field_number = (args[:password_field_number] || 1).to_i
101
+
102
+ # Username configuration
103
+ @username_hostname = args[:username_hostname]
104
+ @username_is_email_address = args[:username_is_email_address] || false
105
+ @username_prefilled = args[:username_prefilled] || false
106
+
107
+ # Runtime configuration
108
+ @debug = args[:debug] || false
109
+ @verbose = args[:verbose] || false
110
+ @iterations = args[:iterations] && args[:iterations].to_i
111
+ @delay = (args[:delay] || 0).to_i
112
+ @word_list_path = args[:word_list_path] || '/usr/share/dict/words'
113
+ end
114
+
115
+ def random_user_agent
116
+ AVAILABLE_USER_AGENTS.sample
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,49 @@
1
+ # lib/Moby/RandomInputGenerator.rb
2
+ # Moby::RandomInputGenerator
3
+
4
+ class Moby
5
+ class RandomInputGenerator
6
+ TLD = %w{com net org edu int mil gov arpa biz aero name coop info pro museum}
7
+
8
+ def random_username
9
+ if username_is_email_address?
10
+ if @username_hostname
11
+ "#{random_word}@#{@username_hostname}"
12
+ else
13
+ "#{random_word}@#{random_word}.#{random_TLD}"
14
+ end
15
+ else
16
+ random_word
17
+ end
18
+ end
19
+
20
+ def random_password
21
+ random_word
22
+ end
23
+
24
+ private
25
+
26
+ def initialize(word_list_path:, username_hostname: nil, username_is_email_address: false)
27
+ @word_list_path = word_list_path
28
+ @username_hostname = username_hostname
29
+ @username_is_email_address = username_is_email_address
30
+ end
31
+
32
+ def random_word
33
+ words.sample
34
+ end
35
+
36
+ def words
37
+ @words ||= File.readlines(@word_list_path).map(&:chomp).reject(&:empty?)
38
+ end
39
+ alias_method :load_words, :words
40
+
41
+ def random_TLD
42
+ TLD.sample
43
+ end
44
+
45
+ def username_is_email_address?
46
+ @username_is_email_address
47
+ end
48
+ end
49
+ end
data/lib/Moby/VERSION.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # Moby::VERSION
3
3
 
4
4
  class Moby
5
- VERSION = '1.0.1'
5
+ VERSION = '2.0.0'
6
6
  end
data/lib/Moby.rb CHANGED
@@ -1,179 +1,34 @@
1
1
  # Moby.rb
2
2
  # Moby
3
3
 
4
- require 'mechanize'
5
- require 'pp'
6
-
7
- require_relative './File/self.collect'
8
4
  require_relative './Moby/VERSION'
5
+ require_relative './Moby/Configuration'
6
+ require_relative './Moby/RandomInputGenerator'
7
+ require_relative './Moby/Backends'
9
8
 
10
9
  class Moby
11
- TLD = %w{com net org edu int mil gov arpa biz aero name coop info pro museum}
12
-
13
- attr_accessor\
14
- :debug,
15
- :form_name,
16
- :form_number,
17
- :password_field_name,
18
- :password_field_number,
19
- :url,
20
- :username_field_name,
21
- :username_field_number,
22
- :username_hostname,
23
- :username_is_email_address,
24
- :verbose
25
-
26
- attr_writer\
27
- :user_agent
28
-
29
- def initialize(
30
- debug: false,
31
- form_name: nil,
32
- form_number: 0,
33
- password_field_name: 'password',
34
- password_field_number: 1,
35
- url: nil,
36
- user_agent: nil,
37
- username_field_name: 'username',
38
- username_field_number: 0,
39
- username_hostname: nil,
40
- username_is_email_address: false,
41
- verbose: false
42
- )
43
- @debug = debug
44
- @form_name = form_name
45
- @form_number = form_number.to_i
46
- @password_field_name = password_field_name
47
- @password_field_number = password_field_number.to_i
48
- @url = url
49
- @user_agent = user_agent
50
- @username_field_name = username_field_name
51
- @username_field_number = username_field_number.to_i
52
- @username_hostname = username_hostname
53
- @username_is_email_address = username_is_email_address
54
- @verbose = verbose
55
- end
56
-
57
10
  def counter_phish
58
- start_time = Time.now
59
- puts "Moby session begun #{start_time}."
60
- submission_count = 0
61
- begin
62
- loop do
63
- mechanize.user_agent_alias = user_agent
64
- username_field.value = username
65
- password_field.value = password
66
- pp page if @debug
67
- result = mechanize.submit(form)
68
- pp result if @debug
69
- submission_count += 1
70
- puts "#{submission_count} #{username}:#{password}" if @verbose
71
- rescue Net::HTTP::Persistent::Error
72
- puts "\n\nNet::HTTP::Persistent::Error rescued.\n\n" if @verbose
73
- rescue Net::OpenTimeout
74
- puts "\n\nNet::OpenTimeout rescued.\n\n" if @verbose
75
- end
76
- ensure
77
- finish_time = Time.now
78
- time_delta_in_minutes = (finish_time - start_time) / 60
79
- submissions_per_minute = submission_count / time_delta_in_minutes
80
- puts "Moby session terminated #{finish_time} with #{submission_count} counter-phishes served in #{time_delta_in_minutes} minutes for an average of #{submissions_per_minute} submissions per minute."
81
- end
11
+ backend.counter_phish
82
12
  end
83
13
 
84
14
  private
85
15
 
86
- def mechanize
87
- @mechanize ||= Mechanize.new
88
- end
89
-
90
- def words(filename = '/usr/share/dict/words')
91
- @words ||= File.collect(filename){|line| line.chomp}
92
- end
93
-
94
- def random_word
95
- words[rand(words.size)]
96
- end
97
-
98
- def random_TLD
99
- TLD[rand(TLD.size)]
100
- end
101
-
102
- def random_user_agent
103
- agent_aliases = Mechanize::AGENT_ALIASES
104
- agent_aliases.delete('Mechanize')
105
- agent_aliases_keys = agent_aliases.keys
106
- agent_aliases_keys[rand(agent_aliases.size)]
16
+ def initialize(**args)
17
+ @configuration = Configuration.new(**args)
107
18
  end
108
19
 
109
- def page
110
- @page ||= (
111
- page = mechanize.get(@url)
112
- md = page.body.match(/meta.*?Refresh.*?url=(.*?)"/i)
113
- if md
114
- puts 'meta_refresh_url found' if @debug
115
- page = mechanize.get(md[1])
116
- end
117
- pp page if @debug
118
- page
20
+ def backend
21
+ @backend ||= Backends.for(
22
+ config: @configuration,
23
+ random_input_generator: random_input_generator
119
24
  )
120
25
  end
121
26
 
122
- def form
123
- @form ||= (
124
- if @form_name
125
- page.form(@form_name)
126
- elsif @form_number
127
- page.forms[@form_number]
128
- else
129
- page.forms[0]
130
- end
27
+ def random_input_generator
28
+ @random_input_generator ||= RandomInputGenerator.new(
29
+ word_list_path: @configuration.word_list_path,
30
+ username_hostname: @configuration.username_hostname,
31
+ username_is_email_address: @configuration.username_is_email_address?
131
32
  )
132
33
  end
133
-
134
- def username
135
- if username_is_email_address?
136
- if username_hostname
137
- "#{random_word}@#{@username_hostname}"
138
- else
139
- "#{random_word}@#{random_word}.#{random_TLD}"
140
- end
141
- else
142
- random_word
143
- end
144
- end
145
-
146
- def password
147
- random_word
148
- end
149
-
150
- def user_agent
151
- @user_agent || random_user_agent
152
- end
153
-
154
- def username_field
155
- if @username_field_name
156
- form.field(@username_field_name)
157
- elsif @username_field_number
158
- form.fields[@username_field_number]
159
- else
160
- form.fields[0]
161
- end
162
- end
163
-
164
- def password_field
165
- if @password_field_name
166
- form.field(@password_field_name)
167
- elsif @password_field_number
168
- form.fields[@password_field_number]
169
- else
170
- form.fields[1]
171
- end
172
- end
173
-
174
- # predicate methods
175
-
176
- def username_is_email_address?
177
- @username_is_email_address || @username_hostname || username_field.name == 'email'
178
- end
179
34
  end
@@ -0,0 +1,18 @@
1
+ # Selenium/WebDriver/Driver/Attempt/attempt.rb
2
+ # Selenium::WebDriver::Driver::Attempt.attempt
3
+
4
+ # 20200418
5
+ # 0.3.0
6
+
7
+ # Examples:
8
+ # 1. driver.attempt do |driver|
9
+ # driver.get('https://example.com/users/sign_in')
10
+ # enter_username
11
+ # enter_password
12
+ # sign_in
13
+ # end
14
+
15
+ # Changes:
16
+ # 1. Moved all the logic into the Thoran namespace.
17
+
18
+ require 'Thoran/Selenium/WebDriver/Driver/Attempt/attempt'
@@ -0,0 +1,13 @@
1
+ # Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
2
+ # Selenium::WebDriver::SearchContext::ElementPresentQ.element_present?
3
+
4
+ # 20200418
5
+ # 0.3.0
6
+
7
+ # Examples:
8
+ # 1. driver.element_present?(:xpath, '//a[@id="submit"]')
9
+
10
+ # Changes:
11
+ # 1. Moved all the logic into the Thoran namespace.
12
+
13
+ require 'Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ'
@@ -0,0 +1,53 @@
1
+ # Thoran/Selenium/WebDriver/Attempt/attempt.rb
2
+ # Thoran::Selenium::WebDriver::Attempt.attempt
3
+
4
+ # 20200419
5
+ # 0.0.1
6
+
7
+ # Examples:
8
+ # 1. driver.attempt do |driver|
9
+ # driver.get('https://example.com/users/sign_in')
10
+ # enter_username
11
+ # enter_password
12
+ # sign_in
13
+ # end
14
+
15
+ # Changes:
16
+ # 1. + Thoran namespace.
17
+
18
+ module Thoran
19
+ module Selenium
20
+ module WebDriver
21
+ module Driver
22
+ module Attempt
23
+
24
+ def attempt(max_attempts = 3, &block)
25
+ attempts = 0
26
+ loop do
27
+ begin
28
+ yield
29
+ break
30
+ rescue Timeout::Error, ::Selenium::WebDriver::Error::UnknownError, ::Selenium::WebDriver::Error::NoSuchElementError => e
31
+ attempts += 1
32
+ if attempts >= max_attempts
33
+ @driver.quit
34
+ puts "Giving up after #{max_attempts} attempts to #{__method__} because #{e}."
35
+ exit
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ module Selenium
48
+ module WebDriver
49
+ class Driver
50
+ include Thoran::Selenium::WebDriver::Driver::Attempt
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,39 @@
1
+ # Thoran/Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ.rb
2
+ # Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ.element_present?
3
+
4
+ # 20200418
5
+ # 0.0.0
6
+
7
+ # Examples:
8
+ # 1. driver.element_present?(:xpath, '//a[@id="submit"]')
9
+ # 2. driver.element_present?(:id, @logout_button_id)
10
+
11
+ # Changes:
12
+ # 1. + Thoran namespace.
13
+
14
+ module Thoran
15
+ module Selenium
16
+ module WebDriver
17
+ module SearchContext
18
+ module ElementPresentQ
19
+
20
+ def element_present?(selector_type, selector)
21
+ bridge.find_element_by(selector_type, selector)
22
+ true
23
+ rescue ::Selenium::WebDriver::Error::NoSuchElementError
24
+ false
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ module Selenium
34
+ module WebDriver
35
+ class Driver
36
+ include Thoran::Selenium::WebDriver::SearchContext::ElementPresentQ
37
+ end
38
+ end
39
+ end
data/moby.rb.gemspec CHANGED
@@ -16,9 +16,10 @@ Gem::Specification.new do |spec|
16
16
  spec.homepage = 'https://github.com/thoran/moby'
17
17
  spec.license = 'MIT'
18
18
 
19
- spec.required_ruby_version = ">= 2.5.0"
19
+ spec.required_ruby_version = ">= 3.3.0"
20
20
 
21
21
  spec.add_dependency('mechanize', '~> 2')
22
+ spec.add_dependency('selenium-webdriver', '~> 4')
22
23
  spec.add_dependency('switches.rb', '~> 0')
23
24
 
24
25
  spec.add_development_dependency('minitest')
@@ -0,0 +1,83 @@
1
+ # test/Moby/Backends/MechanizeBackend_test.rb
2
+
3
+ require_relative '../../test_helper'
4
+
5
+ describe Moby::Backends::MechanizeBackend do
6
+ let(:url){'https://phishing.example.com/login'}
7
+ let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
8
+
9
+ def backend(**config_args)
10
+ config = Moby::Configuration.new(url: url, **config_args)
11
+ Moby::Backends::MechanizeBackend.new(config: config, random_input_generator: generator)
12
+ end
13
+
14
+ before do
15
+ WebMock.disable_net_connect!
16
+ stub_request(:get, url)
17
+ .to_return(status: 200, body: TestFixtures::SIMPLE_LOGIN_FORM, headers: {'Content-Type' => 'text/html'})
18
+ stub_request(:post, url).to_return(status: 200, body: 'Success')
19
+ end
20
+
21
+ after do
22
+ WebMock.reset!
23
+ WebMock.allow_net_connect!
24
+ end
25
+
26
+ describe "page loading" do
27
+ it "loads the target url once and memoises the page" do
28
+ b = backend
29
+ page1 = b.send(:page)
30
+ page2 = b.send(:page)
31
+
32
+ _(page1).must_be_instance_of(Mechanize::Page)
33
+ _(page1).must_be_same_as(page2)
34
+ assert_requested(:get, url, times: 1)
35
+ end
36
+ end
37
+
38
+ describe "form finding" do
39
+ it "finds the form by name" do
40
+ form = backend(form_name: 'login').send(:form)
41
+
42
+ _(form).must_be_instance_of(Mechanize::Form)
43
+ _(form.name).must_equal('login')
44
+ end
45
+
46
+ it "finds the form by number" do
47
+ multi_url = 'https://multi.example.com/page'
48
+ stub_request(:get, multi_url)
49
+ .to_return(status: 200, body: TestFixtures::MULTIPLE_FORMS, headers: {'Content-Type' => 'text/html'})
50
+ config = Moby::Configuration.new(url: multi_url, form_number: 1)
51
+ b = Moby::Backends::MechanizeBackend.new(config: config, random_input_generator: generator)
52
+
53
+ _(b.send(:form).node['id']).must_equal('login_form')
54
+ end
55
+ end
56
+
57
+ describe "field finding" do
58
+ it "finds the username and password fields by name" do
59
+ b = backend(form_name: 'login', username_field_name: 'username', password_field_name: 'password')
60
+
61
+ _(b.send(:username_field).name).must_equal('username')
62
+ _(b.send(:password_field).name).must_equal('password')
63
+ end
64
+ end
65
+
66
+ describe "submitting" do
67
+ it "fills both fields and submits" do
68
+ backend.send(:fill_and_submit_login_form)
69
+
70
+ assert_requested(:post, url) do |request|
71
+ request.body.match?(/username=[^&]+/) && request.body.match?(/password=[^&]+/)
72
+ end
73
+ end
74
+
75
+ it "submits only the password when the username is prefilled" do
76
+ backend(username_prefilled: true).send(:fill_and_submit_login_form)
77
+
78
+ assert_requested(:post, url) do |request|
79
+ request.body.match?(/username=(&|\z)/) && request.body.match?(/password=[^&]+/)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,44 @@
1
+ # test/Moby/Backends/SeleniumBackend_test.rb
2
+
3
+ require_relative '../../test_helper'
4
+
5
+ describe Moby::Backends::SeleniumBackend do
6
+ let(:url){'https://phishing.example.com/login'}
7
+ let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
8
+
9
+ def backend(**config_args)
10
+ config = Moby::Configuration.new(url: url, backend: :selenium, **config_args)
11
+ Moby::Backends::SeleniumBackend.new(config: config, random_input_generator: generator)
12
+ end
13
+
14
+ # Stub the real browser launch so no Chrome/Firefox process is started.
15
+ def with_stubbed_driver
16
+ built = 0
17
+ fake_driver = Object.new
18
+ original = Selenium::WebDriver.method(:for)
19
+ Selenium::WebDriver.define_singleton_method(:for) do |*_args, **_kwargs|
20
+ built += 1
21
+ fake_driver
22
+ end
23
+ begin
24
+ yield(fake_driver, ->{built})
25
+ ensure
26
+ Selenium::WebDriver.define_singleton_method(:for, original)
27
+ end
28
+ end
29
+
30
+ describe "#driver" do
31
+ it "builds the driver once and memoises it (returning a non-nil driver)" do
32
+ with_stubbed_driver do |fake_driver, build_count|
33
+ b = backend
34
+ first = b.send(:driver)
35
+ second = b.send(:driver)
36
+
37
+ _(first).wont_be_nil
38
+ _(first).must_be_same_as(fake_driver)
39
+ _(first).must_be_same_as(second)
40
+ _(build_count.call).must_equal(1)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ # test/Moby/Backends_test.rb
2
+
3
+ require_relative '../test_helper'
4
+
5
+ describe Moby::Backends do
6
+ let(:url){'https://phishing.example.com/login'}
7
+ let(:generator){Moby::RandomInputGenerator.new(word_list_path: TestFixtures::WORD_LIST_PATH)}
8
+
9
+ def backend_for(**config_args)
10
+ config = Moby::Configuration.new(url: url, **config_args)
11
+ Moby::Backends.for(config: config, random_input_generator: generator)
12
+ end
13
+
14
+ describe ".for" do
15
+ it "returns a MechanizeBackend for the mechanize configuration" do
16
+ _(backend_for).must_be_instance_of(Moby::Backends::MechanizeBackend)
17
+ end
18
+
19
+ it "returns a SeleniumBackend for the selenium configuration" do
20
+ _(backend_for(backend: :selenium)).must_be_instance_of(Moby::Backends::SeleniumBackend)
21
+ end
22
+
23
+ it "raises for an unknown backend" do
24
+ _(proc{backend_for(backend: :ftp)}).must_raise(RuntimeError)
25
+ end
26
+ end
27
+ end