arachni 1.0.5 → 1.0.6

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +9 -2
  4. data/components/checks/active/code_injection.rb +5 -5
  5. data/components/checks/active/code_injection_timing.rb +3 -3
  6. data/components/checks/active/no_sql_injection_differential.rb +3 -2
  7. data/components/checks/active/os_cmd_injection.rb +11 -5
  8. data/components/checks/active/os_cmd_injection_timing.rb +11 -4
  9. data/components/checks/active/path_traversal.rb +2 -2
  10. data/components/checks/active/sql_injection.rb +1 -1
  11. data/components/checks/active/sql_injection/patterns/mssql +1 -0
  12. data/components/checks/active/sql_injection_differential.rb +3 -2
  13. data/components/checks/active/unvalidated_redirect.rb +3 -3
  14. data/components/checks/passive/common_directories/directories.txt +2 -0
  15. data/components/checks/passive/common_files/filenames.txt +1 -0
  16. data/lib/arachni/browser.rb +17 -1
  17. data/lib/arachni/check/auditor.rb +5 -2
  18. data/lib/arachni/check/base.rb +30 -5
  19. data/lib/arachni/element/capabilities/analyzable/differential.rb +2 -5
  20. data/lib/arachni/element/capabilities/auditable.rb +3 -1
  21. data/lib/arachni/element/capabilities/with_dom.rb +1 -0
  22. data/lib/arachni/element/capabilities/with_node.rb +1 -1
  23. data/lib/arachni/element/cookie.rb +2 -2
  24. data/lib/arachni/element/form.rb +1 -1
  25. data/lib/arachni/element/header.rb +2 -2
  26. data/lib/arachni/element/link_template.rb +1 -1
  27. data/lib/arachni/framework.rb +21 -1144
  28. data/lib/arachni/framework/parts/audit.rb +282 -0
  29. data/lib/arachni/framework/parts/browser.rb +132 -0
  30. data/lib/arachni/framework/parts/check.rb +86 -0
  31. data/lib/arachni/framework/parts/data.rb +158 -0
  32. data/lib/arachni/framework/parts/platform.rb +34 -0
  33. data/lib/arachni/framework/parts/plugin.rb +61 -0
  34. data/lib/arachni/framework/parts/report.rb +128 -0
  35. data/lib/arachni/framework/parts/scope.rb +40 -0
  36. data/lib/arachni/framework/parts/state.rb +457 -0
  37. data/lib/arachni/http/client.rb +33 -30
  38. data/lib/arachni/http/request.rb +6 -2
  39. data/lib/arachni/issue.rb +55 -1
  40. data/lib/arachni/platform/manager.rb +25 -21
  41. data/lib/arachni/state/framework.rb +7 -1
  42. data/lib/arachni/utilities.rb +10 -0
  43. data/lib/version +1 -1
  44. data/spec/arachni/browser_spec.rb +13 -0
  45. data/spec/arachni/check/auditor_spec.rb +1 -0
  46. data/spec/arachni/check/base_spec.rb +80 -0
  47. data/spec/arachni/element/cookie_spec.rb +2 -2
  48. data/spec/arachni/framework/parts/audit_spec.rb +391 -0
  49. data/spec/arachni/framework/parts/browser_spec.rb +26 -0
  50. data/spec/arachni/framework/parts/check_spec.rb +24 -0
  51. data/spec/arachni/framework/parts/data_spec.rb +187 -0
  52. data/spec/arachni/framework/parts/platform_spec.rb +62 -0
  53. data/spec/arachni/framework/parts/plugin_spec.rb +41 -0
  54. data/spec/arachni/framework/parts/report_spec.rb +66 -0
  55. data/spec/arachni/framework/parts/scope_spec.rb +86 -0
  56. data/spec/arachni/framework/parts/state_spec.rb +528 -0
  57. data/spec/arachni/framework_spec.rb +17 -1344
  58. data/spec/arachni/http/client_spec.rb +12 -7
  59. data/spec/arachni/issue_spec.rb +35 -0
  60. data/spec/arachni/platform/manager_spec.rb +2 -3
  61. data/spec/arachni/state/framework_spec.rb +15 -0
  62. data/spec/components/checks/active/code_injection_timing_spec.rb +5 -5
  63. data/spec/components/checks/active/no_sql_injection_differential_spec.rb +4 -0
  64. data/spec/components/checks/active/os_cmd_injection_spec.rb +20 -7
  65. data/spec/components/checks/active/os_cmd_injection_timing_spec.rb +5 -5
  66. data/spec/components/checks/active/sql_injection_differential_spec.rb +4 -0
  67. data/spec/components/checks/active/sql_injection_spec.rb +2 -3
  68. data/spec/support/servers/arachni/browser.rb +31 -0
  69. data/spec/support/servers/checks/active/code_injection.rb +1 -1
  70. data/spec/support/servers/checks/active/no_sql_injection_differential.rb +36 -34
  71. data/spec/support/servers/checks/active/os_cmd_injection.rb +6 -12
  72. data/spec/support/servers/checks/active/os_cmd_injection_timing.rb +9 -4
  73. data/spec/support/servers/checks/active/sql_injection.rb +1 -1
  74. data/spec/support/servers/checks/active/sql_injection_differential.rb +37 -34
  75. data/spec/support/shared/element/capabilities/with_node.rb +25 -0
  76. data/spec/support/shared/framework.rb +26 -0
  77. data/ui/cli/output.rb +2 -0
  78. data/ui/cli/rpc/server/dispatcher/option_parser.rb +1 -1
  79. metadata +32 -4
  80. data/components/checks/active/sql_injection/patterns/coldfusion +0 -1
@@ -0,0 +1,282 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides {Page} audit functionality and everything related to it, like
14
+ # handling the {Session} and {Trainer}.
15
+ #
16
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
17
+ module Audit
18
+ include Support::Mixins::Observable
19
+
20
+ # @!method on_page_audit( &block )
21
+ advertise :on_page_audit
22
+
23
+ # @!method after_page_audit( &block )
24
+ advertise :after_page_audit
25
+
26
+ # @return [Trainer]
27
+ attr_reader :trainer
28
+
29
+ # @return [Session]
30
+ # Web application session manager.
31
+ attr_reader :session
32
+
33
+ # @return [Arachni::HTTP]
34
+ attr_reader :http
35
+
36
+ # @return [Array<String>]
37
+ # Page URLs which elicited no response from the server and were not audited.
38
+ # Not determined by HTTP status codes, we're talking network failures here.
39
+ attr_reader :failures
40
+
41
+ def initialize
42
+ super
43
+
44
+ @http = HTTP::Client.instance
45
+
46
+ # Holds page URLs which returned no response.
47
+ @failures = []
48
+ @retries = {}
49
+
50
+ @current_url = ''
51
+
52
+ reset_session
53
+ reset_trainer
54
+ end
55
+
56
+ # @note Will update the {HTTP::Client#cookie_jar} with {Page#cookie_jar}.
57
+ # @note It will audit just the given `page` and not any subsequent pages
58
+ # discovered by the {Trainer} -- i.e. ignore any new elements that might
59
+ # appear as a result.
60
+ # @note It will pass the `page` to the {BrowserCluster} for analysis if the
61
+ # {Page::Scope#dom_depth_limit_reached? DOM depth limit} has
62
+ # not been reached and push resulting pages to {#push_to_page_queue} but
63
+ # will not audit those pages either.
64
+ #
65
+ # @param [Page] page
66
+ # Runs loaded checks against `page`
67
+ def audit_page( page )
68
+ return if !page
69
+
70
+ if page.scope.out?
71
+ print_info "Ignoring page due to exclusion criteria: #{page.dom.url}"
72
+ return false
73
+ end
74
+
75
+ # Initialize the BrowserCluster.
76
+ browser_cluster
77
+
78
+ state.audited_page_count += 1
79
+ add_to_sitemap( page )
80
+ sitemap.merge!( browser_sitemap )
81
+
82
+ print_line
83
+ print_status "[HTTP: #{page.code}] #{page.dom.url}"
84
+
85
+ if page.platforms.any?
86
+ print_info "Identified as: #{page.platforms.to_a.join( ', ' )}"
87
+ end
88
+
89
+ if crawl?
90
+ pushed = push_paths_from_page( page )
91
+ print_info "Analysis resulted in #{pushed.size} usable paths."
92
+ end
93
+
94
+ if host_has_browser?
95
+ print_info "DOM depth: #{page.dom.depth} (Limit: #{options.scope.dom_depth_limit})"
96
+
97
+ if page.dom.transitions.any?
98
+ print_info ' Transitions:'
99
+ page.dom.print_transitions( method(:print_info), ' ' )
100
+ end
101
+ end
102
+
103
+ # Aside from plugins and whatnot, the Trainer hooks here to update the
104
+ # ElementFilter so that it'll know if new elements appear during the
105
+ # audit, so it's a big deal.
106
+ notify_on_page_audit( page )
107
+
108
+ @current_url = page.dom.url.to_s
109
+
110
+ http.update_cookies( page.cookie_jar )
111
+ perform_browser_analysis( page )
112
+
113
+ # Remove elements which have already passed through here.
114
+ pre_audit_element_filter( page )
115
+
116
+ # Run checks which **don't** benefit from fingerprinting first, so that
117
+ # we can use the responses of their HTTP requests to fingerprint the
118
+ # webapp platforms, so that the checks which **do** benefit from knowing
119
+ # the remote platforms can run more efficiently.
120
+ ran = false
121
+ @checks.without_platforms.values.each do |check|
122
+ ran = true if check_page( check, page )
123
+ end
124
+ harvest_http_responses if ran
125
+ run_http = ran
126
+
127
+ ran = false
128
+ @checks.with_platforms.values.each do |check|
129
+ ran = true if check_page( check, page )
130
+ end
131
+ harvest_http_responses if ran
132
+ run_http ||= ran
133
+
134
+ if Arachni::Check::Auditor.has_timeout_candidates?
135
+ print_line
136
+ print_status "Verifying timeout-analysis candidates for: #{page.dom.url}"
137
+ print_info '---------------------------------------'
138
+ Arachni::Check::Auditor.timeout_audit_run
139
+ run_http = true
140
+ end
141
+
142
+ # Makes it easier on the GC.
143
+ page.clear_cache
144
+
145
+ notify_after_page_audit( page )
146
+ run_http
147
+ end
148
+
149
+ private
150
+
151
+ # Performs the audit.
152
+ def audit
153
+ handle_signals
154
+ return if aborted?
155
+
156
+ state.status = :scanning if !pausing?
157
+
158
+ push_to_url_queue( options.url )
159
+ options.scope.extend_paths.each { |url| push_to_url_queue( url ) }
160
+ options.scope.restrict_paths.each { |url| push_to_url_queue( url, true ) }
161
+
162
+ # Initialize the BrowserCluster.
163
+ browser_cluster
164
+
165
+ # Keep auditing until there are no more resources in the queues and the
166
+ # browsers have stopped spinning.
167
+ loop do
168
+ show_workload_msg = true
169
+ while !has_audit_workload? && wait_for_browser?
170
+ if show_workload_msg
171
+ print_line
172
+ print_status 'Workload exhausted, waiting for new pages' <<
173
+ ' from the browser-cluster...'
174
+ end
175
+ show_workload_msg = false
176
+
177
+ last_pending_jobs ||= 0
178
+ pending_jobs = browser_cluster.pending_job_counter
179
+ if pending_jobs != last_pending_jobs
180
+ browser_cluster.print_info "Pending jobs: #{pending_jobs}"
181
+ end
182
+ last_pending_jobs = pending_jobs
183
+
184
+ sleep 0.1
185
+ end
186
+
187
+ audit_queues
188
+
189
+ break if page_limit_reached?
190
+ break if !has_audit_workload? && !wait_for_browser?
191
+ end
192
+ end
193
+
194
+ # Audits the {Data::Framework.url_queue URL} and {Data::Framework.page_queue Page}
195
+ # queues while maintaining a valid session with the webapp if we've got
196
+ # login capabilities.
197
+ def audit_queues
198
+ return if @audit_queues_done == false || !has_audit_workload? ||
199
+ page_limit_reached?
200
+
201
+ @audit_queues_done = false
202
+
203
+ # If for some reason we've got pages in the page queue this early,
204
+ # consume them and get it over with.
205
+ audit_page_queue
206
+
207
+ next_page = nil
208
+ while !suspended? && !page_limit_reached? &&
209
+ (page = next_page || pop_page_from_url_queue)
210
+
211
+ # Helps us schedule the next page to be grabbed along with the audit
212
+ # requests for the current page to avoid blocking.
213
+ next_page = nil
214
+ next_page_call = proc do
215
+ pop_page_from_url_queue { |p| next_page = p }
216
+ end
217
+
218
+ # If we can login capabilities make sure that our session is valid
219
+ # before grabbing and auditing the next page.
220
+ if session.can_login?
221
+ # Schedule the login check to happen along with the audit requests
222
+ # to prevent blocking and grab the next page as well.
223
+ session.logged_in? do |bool|
224
+ next next_page_call.call if bool
225
+
226
+ session.login
227
+ next_page_call
228
+ end
229
+ else
230
+ next_page_call.call
231
+ end
232
+
233
+ # We're counting on piggybacking the next page retrieval with the
234
+ # page audit, however if there wasn't an audit we need to force an
235
+ # HTTP run.
236
+ audit_page( page ) or http.run
237
+
238
+ if next_page && suspend?
239
+ data.page_queue << next_page
240
+ end
241
+
242
+ handle_signals
243
+
244
+ # Consume pages somehow triggered by the audit and pushed by the
245
+ # trainer or plugins or whatever.
246
+ audit_page_queue
247
+ end
248
+
249
+ audit_page_queue
250
+
251
+ @audit_queues_done = true
252
+ true
253
+ end
254
+
255
+ # Audits the page queue.
256
+ #
257
+ # @see #pop_page_from_queue
258
+ def audit_page_queue
259
+ while !suspended? && !page_limit_reached? && (page = pop_page_from_queue)
260
+ audit_page( page )
261
+ handle_signals
262
+ end
263
+ end
264
+
265
+ def harvest_http_responses
266
+ print_status 'Harvesting HTTP responses...'
267
+ print_info 'Depending on server responsiveness and network' <<
268
+ ' conditions this may take a while.'
269
+
270
+ # Run all the queued HTTP requests and harvest the responses.
271
+ http.run
272
+
273
+ # Needed for some HTTP callbacks.
274
+ http.run
275
+ end
276
+
277
+ end
278
+
279
+ end
280
+ end
281
+ end
282
+
@@ -0,0 +1,132 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides access to the {BrowserCluster} and relevant helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Browser
17
+
18
+ # @return [BrowserCluster, nil]
19
+ # A lazy-loaded browser cluster or `nil` if
20
+ # {OptionGroups::BrowserCluster#pool_size} or
21
+ # {OptionGroups::Scope#dom_depth_limit} are 0 or not
22
+ # {#host_has_browser?}.
23
+ def browser_cluster
24
+ return if options.browser_cluster.pool_size == 0 ||
25
+ Options.scope.dom_depth_limit == 0 || !host_has_browser?
26
+
27
+ # Initialization may take a while so since we lazy load this make sure
28
+ # that only one thread gets to this code at a time.
29
+ synchronize do
30
+ if !@browser_cluster
31
+ state.set_status_message :browser_cluster_startup
32
+ end
33
+
34
+ @browser_cluster ||= BrowserCluster.new
35
+ state.clear_status_messages
36
+ @browser_cluster
37
+ end
38
+ end
39
+
40
+ # @return [Bool]
41
+ # `true` if the environment has a browser, `false` otherwise.
42
+ def host_has_browser?
43
+ Arachni::Browser.has_executable?
44
+ end
45
+
46
+ def wait_for_browser?
47
+ @browser_cluster && !browser_cluster.done?
48
+ end
49
+
50
+ def browser_job_skip_states
51
+ return if !@browser_cluster
52
+ browser_cluster.skip_states( browser_job.id )
53
+ end
54
+
55
+ private
56
+
57
+ def shutdown_browser_cluster
58
+ return if !@browser_cluster
59
+
60
+ browser_cluster.shutdown
61
+
62
+ @browser_cluster = nil
63
+ @browser_job = nil
64
+ end
65
+
66
+ def browser_sitemap
67
+ return {} if !@browser_cluster
68
+ browser_cluster.sitemap
69
+ end
70
+
71
+ def browser_job_update_skip_states( states )
72
+ return if states.empty?
73
+ browser_cluster.update_skip_states browser_job.id, states
74
+ end
75
+
76
+ def handle_browser_page( page )
77
+ synchronize do
78
+ return if !push_to_page_queue page
79
+
80
+ pushed_paths = nil
81
+ if crawl?
82
+ pushed_paths = push_paths_from_page( page ).size
83
+ end
84
+
85
+ print_status "Got new page from the browser-cluster: #{page.dom.url}"
86
+ print_info "DOM depth: #{page.dom.depth} (Limit: #{options.scope.dom_depth_limit})"
87
+
88
+ if page.dom.transitions.any?
89
+ print_info ' Transitions:'
90
+ page.dom.print_transitions( method(:print_info), ' ' )
91
+ end
92
+
93
+ if pushed_paths
94
+ print_info " -- Analysis resulted in #{pushed_paths} usable paths."
95
+ end
96
+ end
97
+ end
98
+
99
+ # Passes the `page` to {BrowserCluster#queue} and then pushes
100
+ # the resulting pages to {#push_to_page_queue}.
101
+ #
102
+ # @param [Page] page
103
+ # Page to analyze.
104
+ def perform_browser_analysis( page )
105
+ return if !browser_cluster || !accepts_more_pages? ||
106
+ Options.scope.dom_depth_limit.to_i < page.dom.depth + 1 ||
107
+ !page.has_script?
108
+
109
+ browser_cluster.queue( browser_job.forward( resource: page ) ) do |response|
110
+ handle_browser_page response.page
111
+ end
112
+
113
+ true
114
+ end
115
+
116
+ def browser_job
117
+ # We'll recycle the same job since all of them will have the same
118
+ # callback. This will force the BrowserCluster to use the same block
119
+ # for all queued jobs.
120
+ #
121
+ # Also, this job should never end so that all analysis operations
122
+ # share the same state.
123
+ @browser_job ||= BrowserCluster::Jobs::ResourceExploration.new(
124
+ never_ending: true
125
+ )
126
+ end
127
+
128
+ end
129
+
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,86 @@
1
+ =begin
2
+ Copyright 2010-2014 Tasos Laskos <tasos.laskos@arachni-scanner.com>
3
+
4
+ This file is part of the Arachni Framework project and is subject to
5
+ redistribution and commercial restrictions. Please see the Arachni Framework
6
+ web site for more information on licensing and terms of use.
7
+ =end
8
+
9
+ module Arachni
10
+ class Framework
11
+ module Parts
12
+
13
+ # Provides a {Arachni::Check::Manager} and related helpers.
14
+ #
15
+ # @author Tasos "Zapotek" Laskos <tasos.laskos@arachni-scanner.com>
16
+ module Check
17
+
18
+ # @return [Arachni::Check::Manager]
19
+ attr_reader :checks
20
+
21
+ def initialize
22
+ super
23
+ @checks = Arachni::Check::Manager.new( self )
24
+ end
25
+
26
+ # @return [Array<Hash>]
27
+ # Information about all available {Checks}.
28
+ def list_checks( patterns = nil )
29
+ loaded = @checks.loaded
30
+
31
+ begin
32
+ @checks.clear
33
+ @checks.available.map do |name|
34
+ path = @checks.name_to_path( name )
35
+ next if !list_check?( path, patterns )
36
+
37
+ @checks[name].info.merge(
38
+ shortname: name,
39
+ author: [@checks[name].info[:author]].
40
+ flatten.map { |a| a.strip },
41
+ path: path.strip,
42
+ platforms: @checks[name].platforms,
43
+ elements: @checks[name].elements
44
+ )
45
+ end.compact
46
+ ensure
47
+ @checks.clear
48
+ @checks.load loaded
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ # Passes a page to the check and runs it.
55
+ # It also handles any exceptions thrown by the check at runtime.
56
+ #
57
+ # @param [Check::Base] check
58
+ # Check to run.
59
+ # @param [Page] page
60
+ def check_page( check, page )
61
+ # If we've been given platforms which the check doesn't support don't
62
+ # even bother running it.
63
+ if !check.supports_platforms?( Options.platforms )
64
+ print_info "Check #{check.shortname} does not support: " <<
65
+ Options.platforms.join( ', ' )
66
+ return false
67
+ end
68
+
69
+ begin
70
+ @checks.run_one( check, page )
71
+ rescue => e
72
+ print_error "Error in #{check.to_s}: #{e.to_s}"
73
+ print_error_backtrace e
74
+ false
75
+ end
76
+ end
77
+
78
+ def list_check?( path, patterns = nil )
79
+ regexp_array_match( patterns, path )
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
86
+ end