arachni 1.0.5 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
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