moby.rb 1.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,236 @@
1
+ # lib/Moby/Backends/SeleniumBackend.rb
2
+ # Moby::Backends::SeleniumBackend
3
+
4
+ require 'selenium-webdriver'
5
+
6
+ require_relative '../Selenium/WebDriver/Driver/Attempt/attempt'
7
+ require_relative '../Selenium/WebDriver/SearchContext/ElementPresentQ/element_presentQ'
8
+
9
+ class Moby
10
+ module Backends
11
+ class SeleniumBackend
12
+ def counter_phish
13
+ set_up_driver
14
+ navigate_to_target
15
+ repeatedly_fill_and_submit_login_form
16
+ ensure
17
+ cleanup_driver unless @config.debug?
18
+ end
19
+
20
+ private
21
+
22
+ def initialize(config:, random_input_generator:)
23
+ @config = config
24
+ @random_input_generator = random_input_generator
25
+ @driver = nil
26
+ end
27
+
28
+ def driver
29
+ puts "Setting up #{@config.browser} driver..." if @config.verbose
30
+ case @config.browser.to_sym
31
+ when :chrome
32
+ options = Selenium::WebDriver::Chrome::Options.new
33
+ options.add_argument('--headless') unless @config.debug
34
+ options.add_argument('--disable-gpu')
35
+ options.add_argument('--no-sandbox')
36
+ options.add_argument("user-agent=#{@config.user_agent}") if @config.user_agent
37
+ @driver = Selenium::WebDriver.for(:chrome, options: options)
38
+ when :firefox
39
+ options = Selenium::WebDriver::Firefox::Options.new
40
+ options.add_argument('--headless') unless @config.debug
41
+ options.add_preference('general.useragent.override', @config.user_agent) if @config.user_agent
42
+ @driver = Selenium::WebDriver.for(:firefox, options: options)
43
+ else
44
+ @driver = Selenium::WebDriver.for(@config.browser)
45
+ end
46
+ puts "Driver setup complete." if @config.verbose?
47
+ end
48
+ alias_method :set_up_driver, :driver
49
+
50
+ def navigate_to_target
51
+ puts "Navigating to #{@config.url}..." if @config.verbose?
52
+ @driver.attempt do
53
+ @driver.get(@config.url)
54
+ wait_for_page_load
55
+ end
56
+ puts "Page loaded successfully." if @config.verbose?
57
+ end
58
+
59
+ def wait_for_page_load
60
+ wait = Selenium::WebDriver::Wait.new(timeout: 10)
61
+ wait.until do
62
+ @driver.execute_script('return document.readyState') == 'complete'
63
+ end
64
+ end
65
+
66
+ def form
67
+ @form ||= (
68
+ if @config.using_form_name?
69
+ find_form_by_name
70
+ else
71
+ find_form_by_number
72
+ end
73
+ )
74
+ end
75
+
76
+ def repeatedly_fill_and_submit_login_form
77
+ start_time = Time.now
78
+ puts "moby session begun #{start_time}."
79
+ submission_count = 0
80
+ begin
81
+ if @config.fixed_number_of_submissions?
82
+ @config.iterations.times do |iteration|
83
+ fill_and_submit_login_form
84
+ submission_count += 1
85
+ puts "Submission #{submission_count} of #{@config.iterations}." if @config.verbose?
86
+ end
87
+ puts "Successfully completed #{@config.iterations} submissions." if @config.verbose?
88
+ else
89
+ loop do
90
+ fill_and_submit_login_form
91
+ submission_count += 1
92
+ puts "#{submission_count} #{username}:#{password}" if @config.verbose?
93
+ end
94
+ end
95
+ ensure
96
+ finish_time = Time.now
97
+ time_delta_in_minutes = (finish_time - start_time) / 60
98
+ submissions_per_minute = submission_count / time_delta_in_minutes
99
+ 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."
100
+ end
101
+ end
102
+
103
+ def fill_and_submit_login_form
104
+ puts "Filling with: #{username}:#{password}" if @config.verbose?
105
+ driver.navigate.to(@config.url)
106
+ wait_for_page_load
107
+ driver.attempt do
108
+ username_field.clear
109
+ username_field.send_keys(username)
110
+ password_field.clear
111
+ password_field.send_keys(password)
112
+ submit_form(form)
113
+ end
114
+ sleep @config.delay if @config.delay > 0
115
+ end
116
+
117
+ def find_form_by_name
118
+ name = @config.form_name
119
+ selectors = [
120
+ [:name, name],
121
+ [:id, name],
122
+ [:css, "form[name='#{name}']"],
123
+ [:css, "form##{name}"]
124
+ ]
125
+ selectors.each do |type, selector|
126
+ if @driver.element_present?(type, selector)
127
+ return @driver.find_element(type, selector)
128
+ end
129
+ end
130
+ raise("Could not find a form with the name or id of #{@config.form_name}.")
131
+ end
132
+
133
+ def find_form_by_number
134
+ index = @config.form_number
135
+ forms = @driver.find_elements(:tag_name, 'form')
136
+ return forms[index] if forms[index]
137
+ raise("Could not find a form at index #{@config.form_number}.")
138
+ end
139
+
140
+ def username
141
+ @random_input_generator.random_username
142
+ end
143
+
144
+ def password
145
+ @random_input_generator.random_password
146
+ end
147
+
148
+ def username_field
149
+ @username_field ||= (
150
+ if @config.using_username_field_name?
151
+ find_username_field_by_name
152
+ else
153
+ find_username_field_by_number
154
+ end
155
+ )
156
+ end
157
+
158
+ def password_field
159
+ @password_field ||= (
160
+ if @config.using_password_field_name?
161
+ find_password_field_by_name
162
+ else
163
+ find_password_field_by_number
164
+ end
165
+ )
166
+ end
167
+
168
+ def find_username_field_by_name
169
+ name = @config.username_field_name
170
+ selectors = [
171
+ [:name, name],
172
+ [:id, name],
173
+ [:css, "input[name='#{name}']"],
174
+ [:css, "input##{name}"]
175
+ ]
176
+ selectors.each do |type, selector|
177
+ begin
178
+ element = form.find_element(type, selector)
179
+ return element if element
180
+ rescue Selenium::WebDriver::Error::NoSuchElementError
181
+ next
182
+ end
183
+ end
184
+ raise("Could not find username field: #{@config.username_field_name}")
185
+ end
186
+
187
+ def find_username_field_by_number
188
+ index = @config.username_field_number
189
+ inputs = form.find_elements(:tag_name, 'input')
190
+ inputs[index] ||
191
+ raise("Could not find username field at index #{index}")
192
+ end
193
+
194
+ def find_password_field_by_name
195
+ name = @config.password_field_name
196
+ selectors = [
197
+ [:name, name],
198
+ [:id, name],
199
+ [:css, "input[name='#{name}']"],
200
+ [:css, "input##{name}"]
201
+ ]
202
+ selectors.each do |type, selector|
203
+ begin
204
+ element = form.find_element(type, selector)
205
+ return element if element
206
+ rescue Selenium::WebDriver::Error::NoSuchElementError
207
+ next
208
+ end
209
+ end
210
+ raise("Could not find password field: #{@config.password_field_name}")
211
+ end
212
+
213
+ def find_password_field_by_number
214
+ index = @config.password_field_number
215
+ inputs = form.find_elements(:tag_name, 'input')
216
+ inputs[index] ||
217
+ raise("Could not find password field at index #{index}")
218
+ end
219
+
220
+ def submit_form(form)
221
+ begin
222
+ submit_button = form.find_element(:css, "input[type='submit'], button[type='submit'], button")
223
+ submit_button.click
224
+ rescue
225
+ form.submit
226
+ end
227
+ sleep 0.5
228
+ end
229
+
230
+ def cleanup_driver
231
+ @driver.quit if @driver
232
+ puts "Driver closed." if @config.verbose?
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,113 @@
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
+ :iterations,
25
+ :delay,
26
+ :word_list_path
27
+ )
28
+
29
+ attr_writer(
30
+ :user_agent,
31
+ )
32
+
33
+ def user_agent
34
+ @user_agent || random_user_agent
35
+ end
36
+
37
+ def using_form_name?
38
+ !@form_name.nil?
39
+ end
40
+
41
+ def using_username_field_name?
42
+ !@username_field_name.nil?
43
+ end
44
+
45
+ def using_password_field_name?
46
+ !@password_field_name.nil?
47
+ end
48
+
49
+ def mechanize?
50
+ @backend == :mechanize
51
+ end
52
+
53
+ def selenium?
54
+ @backend == :selenium
55
+ end
56
+
57
+ def username_is_email_address?
58
+ @username_is_email_address || @username_hostname || @username_field_name == 'email'
59
+ end
60
+
61
+ def debug?
62
+ @debug
63
+ end
64
+
65
+ def verbose?
66
+ @verbose
67
+ end
68
+
69
+ def fixed_number_of_submissions?
70
+ !!@iterations
71
+ end
72
+
73
+ private
74
+
75
+ # @param url [String] The target URL
76
+ # @param backend [Symbol] :mechanize or :selenium (default: :selenium)
77
+ # @param browser [Symbol] :chrome or :firefox (default: :chrome)
78
+
79
+ def initialize(url:, backend: :selenium, browser: :chrome, **args)
80
+ @url = url
81
+ @backend = backend.to_sym
82
+
83
+ # Browser configuration (for Selenium)
84
+ @browser = browser.to_sym
85
+ @user_agent = args[:user_agent]
86
+
87
+ # Form identification
88
+ @form_name = args[:form_name]
89
+ @form_number = args[:form_number] || 0
90
+
91
+ # Field identification
92
+ @username_field_name = args[:username_field_name]
93
+ @username_field_number = args[:username_field_number] || 0
94
+ @password_field_name = args[:password_field_name]
95
+ @password_field_number = args[:password_field_number] || 1
96
+
97
+ # Username configuration
98
+ @username_hostname = args[:username_hostname]
99
+ @username_is_email_address = args[:username_is_email_address] || false
100
+
101
+ # Runtime configuration
102
+ @debug = args[:debug] || false
103
+ @verbose = args[:verbose] || false
104
+ @iterations = args[:iterations] || 100
105
+ @delay = args[:delay] || 0
106
+ @word_list_path = args[:word_list_path] || '/usr/share/dict/words'
107
+ end
108
+
109
+ def random_user_agent
110
+ AVAILABLE_USER_AGENTS.sample
111
+ end
112
+ end
113
+ 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
@@ -0,0 +1,6 @@
1
+ # lib/Moby/VERSION.rb
2
+ # Moby::VERSION
3
+
4
+ class Moby
5
+ VERSION = '1.0.0'
6
+ end
data/lib/Moby.rb ADDED
@@ -0,0 +1,179 @@
1
+ # Moby.rb
2
+ # Moby
3
+
4
+ require 'mechanize'
5
+ require 'pp'
6
+
7
+ require_relative './File/self.collect'
8
+ require_relative './Moby/VERSION'
9
+
10
+ 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
+ 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
82
+ end
83
+
84
+ private
85
+
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)]
107
+ end
108
+
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
119
+ )
120
+ end
121
+
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
131
+ )
132
+ 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
+ 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,22 @@
1
+ # Thoran/File/SelfCollect/self.collect.rb
2
+ # Thoran::File::SelfCollect.collect.rb
3
+
4
+ # 20190821
5
+ # 0.2.0
6
+
7
+ # Changes:
8
+ # 1. + Thoran::File::SelfCollect namespace.
9
+
10
+ module Thoran
11
+ module File
12
+ module SelfCollect
13
+
14
+ def collect(filename, &block)
15
+ open(filename, 'r').collect(&block)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
21
+
22
+ File.extend(Thoran::File::SelfCollect)
@@ -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