arachni 1.0.4 → 1.0.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +8 -4
- data/bin/arachni_console +1 -1
- data/components/checks/active/no_sql_injection.rb +4 -4
- data/components/checks/passive/common_directories/directories.txt +1 -0
- data/components/checks/passive/common_files/filenames.txt +1 -0
- data/components/plugins/login_script.rb +156 -0
- data/components/reporters/plugin_formatters/html/login_script.rb +48 -0
- data/components/reporters/plugin_formatters/stdout/login_script.rb +23 -0
- data/components/reporters/plugin_formatters/xml/login_script.rb +26 -0
- data/components/reporters/xml/schema.xsd +17 -0
- data/lib/arachni/browser.rb +7 -4
- data/lib/arachni/browser/javascript.rb +40 -4
- data/lib/arachni/browser/javascript/proxy.rb +1 -1
- data/lib/arachni/browser_cluster/worker.rb +14 -4
- data/lib/arachni/check/auditor.rb +24 -7
- data/lib/arachni/check/manager.rb +6 -0
- data/lib/arachni/framework.rb +54 -6
- data/lib/arachni/http/client.rb +41 -23
- data/lib/arachni/http/headers.rb +5 -1
- data/lib/arachni/http/message.rb +0 -7
- data/lib/arachni/http/request.rb +40 -32
- data/lib/arachni/http/response.rb +8 -1
- data/lib/arachni/platform/manager.rb +7 -0
- data/lib/arachni/rpc/server/framework/multi_instance.rb +1 -1
- data/lib/arachni/session.rb +88 -58
- data/lib/arachni/state/framework.rb +34 -5
- data/lib/arachni/support/profiler.rb +2 -0
- data/lib/arachni/uri.rb +2 -1
- data/lib/version +1 -1
- data/spec/arachni/browser/javascript_spec.rb +15 -0
- data/spec/arachni/check/manager_spec.rb +17 -0
- data/spec/arachni/framework_spec.rb +4 -2
- data/spec/arachni/http/client_spec.rb +1 -1
- data/spec/arachni/session_spec.rb +80 -37
- data/spec/arachni/state/framework_spec.rb +34 -1
- data/spec/arachni/uri_spec.rb +7 -0
- data/spec/components/plugins/login_script_spec.rb +157 -0
- data/spec/support/servers/plugins/login_script.rb +13 -0
- data/ui/cli/output.rb +26 -9
- metadata +11 -3
data/lib/arachni/http/headers.rb
CHANGED
@@ -20,7 +20,11 @@ class Headers < Hash
|
|
20
20
|
|
21
21
|
# @param [Headers, Hash] headers
|
22
22
|
def initialize( headers = {} )
|
23
|
-
(headers || {})
|
23
|
+
merge!( headers || {} )
|
24
|
+
end
|
25
|
+
|
26
|
+
def merge!( headers )
|
27
|
+
headers.each { |k, v| self[k] = v }
|
24
28
|
end
|
25
29
|
|
26
30
|
# @note `field` will be capitalized appropriately before storing.
|
data/lib/arachni/http/message.rb
CHANGED
@@ -38,7 +38,6 @@ class Message
|
|
38
38
|
# Body.
|
39
39
|
def initialize( options = {} )
|
40
40
|
options.each do |k, v|
|
41
|
-
v = my_dup( v )
|
42
41
|
begin
|
43
42
|
send( "#{k}=", v )
|
44
43
|
rescue NoMethodError
|
@@ -65,12 +64,6 @@ class Message
|
|
65
64
|
@url = Arachni::URI( url ).to_s.freeze
|
66
65
|
end
|
67
66
|
|
68
|
-
private
|
69
|
-
|
70
|
-
def my_dup( value )
|
71
|
-
value.dup rescue value
|
72
|
-
end
|
73
|
-
|
74
67
|
end
|
75
68
|
end
|
76
69
|
end
|
data/lib/arachni/http/request.rb
CHANGED
@@ -127,10 +127,10 @@ class Request < Message
|
|
127
127
|
@train = false if @train.nil?
|
128
128
|
@update_cookies = false if @update_cookies.nil?
|
129
129
|
@follow_location = false if @follow_location.nil?
|
130
|
-
@max_redirects = (
|
130
|
+
@max_redirects = (Options.http.request_redirect_limit || REDIRECT_LIMIT)
|
131
131
|
@on_complete = []
|
132
132
|
|
133
|
-
@timeout ||=
|
133
|
+
@timeout ||= Options.http.request_timeout
|
134
134
|
@mode ||= :async
|
135
135
|
@parameters ||= {}
|
136
136
|
@cookies ||= {}
|
@@ -213,6 +213,10 @@ class Request < Message
|
|
213
213
|
end.merge( cookies )
|
214
214
|
end
|
215
215
|
|
216
|
+
def effective_parameters
|
217
|
+
Utilities.uri_parse_query( url ).merge( parameters || {} )
|
218
|
+
end
|
219
|
+
|
216
220
|
def body_parameters
|
217
221
|
return {} if method != :post
|
218
222
|
parameters.any? ? parameters : self.class.parse_body( body )
|
@@ -276,17 +280,10 @@ class Request < Message
|
|
276
280
|
#
|
277
281
|
# @return [Response]
|
278
282
|
def run
|
279
|
-
|
280
|
-
fill_in_data_from_typhoeus_response response
|
281
|
-
|
282
|
-
Response.from_typhoeus( response ).tap { |r| r.request = self }
|
283
|
+
client_run.tap { |r| r.request = self }
|
283
284
|
end
|
284
285
|
|
285
|
-
def handle_response( response
|
286
|
-
if typhoeus_response
|
287
|
-
fill_in_data_from_typhoeus_response typhoeus_response
|
288
|
-
end
|
289
|
-
|
286
|
+
def handle_response( response )
|
290
287
|
response.request = self
|
291
288
|
@on_complete.each { |b| b.call response }
|
292
289
|
response
|
@@ -295,24 +292,15 @@ class Request < Message
|
|
295
292
|
# @return [Typhoeus::Response]
|
296
293
|
# `self` converted to a `Typhoeus::Request`.
|
297
294
|
def to_typhoeus
|
298
|
-
|
299
|
-
map { |k, v| "#{Cookie.encode( k )}=#{Cookie.encode( v )}" }.
|
300
|
-
join( ';' )
|
301
|
-
|
302
|
-
headers['User-Agent'] ||= Arachni::Options.http.user_agent
|
303
|
-
headers['Accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
304
|
-
headers['From'] ||= Arachni::Options.authorized_by if Arachni::Options.authorized_by
|
305
|
-
|
306
|
-
headers.delete( 'Cookie' ) if headers['Cookie'].empty?
|
307
|
-
headers.each { |k, v| headers[k] = Header.encode( v ) if v }
|
295
|
+
prepare_headers
|
308
296
|
|
309
|
-
if (userpwd = (@username ||
|
310
|
-
if (passwd = (@password ||
|
297
|
+
if (userpwd = (@username || Options.http.authentication_username))
|
298
|
+
if (passwd = (@password || Options.http.authentication_password))
|
311
299
|
userpwd += ":#{passwd}"
|
312
300
|
end
|
313
301
|
end
|
314
302
|
|
315
|
-
max_size = @response_max_size ||
|
303
|
+
max_size = @response_max_size || Options.http.response_max_size
|
316
304
|
# Weird I know, for some reason 0 gets ignored.
|
317
305
|
max_size = 1 if max_size == 0
|
318
306
|
|
@@ -320,8 +308,7 @@ class Request < Message
|
|
320
308
|
method: method,
|
321
309
|
headers: headers,
|
322
310
|
body: body,
|
323
|
-
params:
|
324
|
-
merge( parameters || {} ),
|
311
|
+
params: effective_parameters,
|
325
312
|
userpwd: userpwd,
|
326
313
|
followlocation: follow_location?,
|
327
314
|
maxredirs: @max_redirects,
|
@@ -350,15 +337,15 @@ class Request < Message
|
|
350
337
|
options[:proxyuserpwd] = proxy_user_password
|
351
338
|
end
|
352
339
|
|
353
|
-
elsif
|
340
|
+
elsif Options.http.proxy_host && Options.http.proxy_port
|
354
341
|
options.merge!(
|
355
|
-
proxy: "#{
|
356
|
-
proxytype: (
|
342
|
+
proxy: "#{Options.http.proxy_host}:#{Options.http.proxy_port}",
|
343
|
+
proxytype: (Options.http.proxy_type || :http).to_sym
|
357
344
|
)
|
358
345
|
|
359
|
-
if
|
346
|
+
if Options.http.proxy_username && Options.http.proxy_password
|
360
347
|
options[:proxyuserpwd] =
|
361
|
-
"#{
|
348
|
+
"#{Options.http.proxy_username}:#{Options.http.proxy_password}"
|
362
349
|
end
|
363
350
|
end
|
364
351
|
|
@@ -367,7 +354,8 @@ class Request < Message
|
|
367
354
|
|
368
355
|
if @on_complete.any?
|
369
356
|
r.on_complete do |typhoeus_response|
|
370
|
-
|
357
|
+
fill_in_data_from_typhoeus_response typhoeus_response
|
358
|
+
handle_response Response.from_typhoeus( typhoeus_response )
|
371
359
|
end
|
372
360
|
end
|
373
361
|
|
@@ -461,11 +449,31 @@ class Request < Message
|
|
461
449
|
|
462
450
|
private
|
463
451
|
|
452
|
+
def prepare_headers
|
453
|
+
headers['Cookie'] = effective_cookies.
|
454
|
+
map { |k, v| "#{Cookie.encode( k )}=#{Cookie.encode( v )}" }.
|
455
|
+
join( ';' )
|
456
|
+
|
457
|
+
headers['User-Agent'] ||= Options.http.user_agent
|
458
|
+
headers['Accept'] ||= 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
|
459
|
+
headers['From'] ||= Options.authorized_by if Options.authorized_by
|
460
|
+
|
461
|
+
headers.delete( 'Cookie' ) if headers['Cookie'].empty?
|
462
|
+
headers.each { |k, v| headers[k] = Header.encode( v ) if v }
|
463
|
+
end
|
464
|
+
|
464
465
|
def fill_in_data_from_typhoeus_response( response )
|
465
466
|
@headers_string = response.debug_info.header_out.first
|
466
467
|
@effective_body = response.debug_info.data_out.first
|
467
468
|
end
|
468
469
|
|
470
|
+
def client_run
|
471
|
+
response = to_typhoeus.run
|
472
|
+
fill_in_data_from_typhoeus_response response
|
473
|
+
|
474
|
+
Response.from_typhoeus( response )
|
475
|
+
end
|
476
|
+
|
469
477
|
end
|
470
478
|
end
|
471
479
|
end
|
@@ -197,8 +197,15 @@ class Response < Message
|
|
197
197
|
|
198
198
|
def self.from_typhoeus( response )
|
199
199
|
redirections = response.redirections.map do |redirect|
|
200
|
+
rurl = URI.to_absolute( redirect.headers['Location'],
|
201
|
+
response.effective_url )
|
202
|
+
rurl ||= response.effective_url
|
203
|
+
|
204
|
+
# Broken redirection, skip it...
|
205
|
+
next if !rurl
|
206
|
+
|
200
207
|
new(
|
201
|
-
url:
|
208
|
+
url: rurl,
|
202
209
|
code: redirect.code,
|
203
210
|
headers: redirect.headers
|
204
211
|
)
|
@@ -294,6 +294,13 @@ class Manager
|
|
294
294
|
# @return [Manager]
|
295
295
|
# Platform for the given `uri`
|
296
296
|
def self.[]( uri )
|
297
|
+
# If fingerprinting is disabled there's no point in filling the cache
|
298
|
+
# with the same object over and over, create an identical one for all
|
299
|
+
# URLs and return that always.
|
300
|
+
if !Options.fingerprint?
|
301
|
+
return @default ||= new( Options.platforms )
|
302
|
+
end
|
303
|
+
|
297
304
|
return new if !(key = make_key( uri ))
|
298
305
|
synchronize { @platforms[key] ||= new }
|
299
306
|
end
|
@@ -44,7 +44,7 @@ module MultiInstance
|
|
44
44
|
def errors( starting_line = 0, &block )
|
45
45
|
return [] if !File.exists? error_logfile
|
46
46
|
|
47
|
-
error_strings =
|
47
|
+
error_strings = error_buffer.dup
|
48
48
|
|
49
49
|
if starting_line != 0
|
50
50
|
error_strings = error_strings[starting_line..-1]
|
data/lib/arachni/session.rb
CHANGED
@@ -51,6 +51,9 @@ class Session
|
|
51
51
|
# @return [Browser]
|
52
52
|
attr_reader :browser
|
53
53
|
|
54
|
+
# @return [Block]
|
55
|
+
attr_reader :login_sequence
|
56
|
+
|
54
57
|
def clean_up
|
55
58
|
configuration.clear
|
56
59
|
shutdown_browser
|
@@ -98,7 +101,7 @@ class Session
|
|
98
101
|
# @return [Bool]
|
99
102
|
# `true` if {#configure configured}, `false` otherwise.
|
100
103
|
def configured?
|
101
|
-
configuration.any?
|
104
|
+
!!@login_sequence || configuration.any?
|
102
105
|
end
|
103
106
|
|
104
107
|
# Finds a login forms based on supplied location, collection and criteria.
|
@@ -201,16 +204,95 @@ class Session
|
|
201
204
|
end
|
202
205
|
end
|
203
206
|
|
204
|
-
#
|
207
|
+
# @param [Block] block
|
208
|
+
# Login sequence. Must return the resulting {Page}.
|
209
|
+
#
|
210
|
+
# If a {#browser} is {#has_browser? available} it will be passed to the
|
211
|
+
# block.
|
212
|
+
def record_login_sequence( &block )
|
213
|
+
@login_sequence = block
|
214
|
+
end
|
215
|
+
|
216
|
+
# Uses the information provided by {#configure} or {#login_sequence} to login.
|
205
217
|
#
|
206
218
|
# @return [Page, nil]
|
207
|
-
# {
|
208
|
-
# `nil` if not {#configured?}.
|
219
|
+
# {Page} if the login was successful, `nil` otherwise.
|
209
220
|
#
|
221
|
+
# @raise [Error::NotConfigured]
|
222
|
+
# If not {#configured?}.
|
210
223
|
# @raise [Error::FormNotFound]
|
211
224
|
# If the form could not be found.
|
212
225
|
def login
|
213
|
-
fail Error::NotConfigured, 'Please
|
226
|
+
fail Error::NotConfigured, 'Please configure the session first.' if !configured?
|
227
|
+
|
228
|
+
refresh_browser
|
229
|
+
|
230
|
+
page = @login_sequence ? login_from_sequence : login_from_configuration
|
231
|
+
|
232
|
+
if has_browser?
|
233
|
+
http.update_cookies browser.cookies
|
234
|
+
end
|
235
|
+
|
236
|
+
page
|
237
|
+
end
|
238
|
+
|
239
|
+
# @param [Block] block
|
240
|
+
# Block to be passed the {#browser}.
|
241
|
+
def with_browser( &block )
|
242
|
+
block.call browser
|
243
|
+
end
|
244
|
+
|
245
|
+
# @param [Hash] http_options
|
246
|
+
# HTTP options to use for the check.
|
247
|
+
# @param [Block] block
|
248
|
+
# If a block has been provided the check will be async and the result will
|
249
|
+
# be passed to it, otherwise the method will return the result.
|
250
|
+
#
|
251
|
+
# @return [Bool, nil]
|
252
|
+
# `true` if we're logged-in, `false` otherwise.
|
253
|
+
#
|
254
|
+
# @raise [Error::NoLoginCheck]
|
255
|
+
# If no login-check has been configured.
|
256
|
+
def logged_in?( http_options = {}, &block )
|
257
|
+
fail Error::NoLoginCheck if !has_login_check?
|
258
|
+
|
259
|
+
http_options = http_options.merge(
|
260
|
+
mode: block_given? ? :async : :sync,
|
261
|
+
follow_location: true
|
262
|
+
)
|
263
|
+
|
264
|
+
bool = nil
|
265
|
+
http.get( Options.session.check_url, http_options ) do |response|
|
266
|
+
bool = !!response.body.match( Options.session.check_pattern )
|
267
|
+
block.call( bool ) if block
|
268
|
+
end
|
269
|
+
bool
|
270
|
+
end
|
271
|
+
|
272
|
+
# @return [Bool]
|
273
|
+
# `true` if a login check exists, `false` otherwise.
|
274
|
+
def has_login_check?
|
275
|
+
!!@login_check || !!(Options.session.check_url && Options.session.check_pattern)
|
276
|
+
end
|
277
|
+
|
278
|
+
# @return [HTTP::Client]
|
279
|
+
def http
|
280
|
+
HTTP::Client
|
281
|
+
end
|
282
|
+
|
283
|
+
def has_browser?
|
284
|
+
Browser.has_executable? && Options.scope.dom_depth_limit > 0
|
285
|
+
end
|
286
|
+
|
287
|
+
private
|
288
|
+
|
289
|
+
def login_from_sequence
|
290
|
+
print_debug "Logging in via sequence: #{@login_sequence}"
|
291
|
+
@login_sequence.call browser
|
292
|
+
end
|
293
|
+
|
294
|
+
def login_from_configuration
|
295
|
+
print_debug 'Logging in via configuration.'
|
214
296
|
|
215
297
|
if has_browser?
|
216
298
|
print_debug 'Logging in using browser.'
|
@@ -222,7 +304,7 @@ class Session
|
|
222
304
|
|
223
305
|
# Revert to the Framework DOM Level 1 page handling if no browser
|
224
306
|
# is available.
|
225
|
-
page =
|
307
|
+
page = has_browser? ?
|
226
308
|
browser.load( configuration[:url], take_snapshot: false ).to_page :
|
227
309
|
Page.from_url( configuration[:url], precision: 1, http: {
|
228
310
|
update_cookies: true
|
@@ -261,8 +343,6 @@ class Session
|
|
261
343
|
print_debug 'Submitting form.'
|
262
344
|
form.submit { |p| page = p }
|
263
345
|
print_debug 'Form submitted.'
|
264
|
-
|
265
|
-
http.update_cookies browser.cookies
|
266
346
|
else
|
267
347
|
page = form.submit(
|
268
348
|
mode: :sync,
|
@@ -281,56 +361,6 @@ class Session
|
|
281
361
|
page
|
282
362
|
end
|
283
363
|
|
284
|
-
# @param [Block] block
|
285
|
-
# Block to be passed the {#browser}.
|
286
|
-
def with_browser( &block )
|
287
|
-
block.call browser
|
288
|
-
end
|
289
|
-
|
290
|
-
# @param [Hash] http_options
|
291
|
-
# HTTP options to use for the check.
|
292
|
-
# @param [Block] block
|
293
|
-
# If a block has been provided the check will be async and the result will
|
294
|
-
# be passed to it, otherwise the method will return the result.
|
295
|
-
#
|
296
|
-
# @return [Bool, nil]
|
297
|
-
# `true` if we're logged-in, `false` otherwise.
|
298
|
-
#
|
299
|
-
# @raise [Error::NoLoginCheck]
|
300
|
-
# If no login-check has been configured.
|
301
|
-
def logged_in?( http_options = {}, &block )
|
302
|
-
fail Error::NoLoginCheck if !has_login_check?
|
303
|
-
|
304
|
-
http_options = http_options.merge(
|
305
|
-
mode: block_given? ? :async : :sync,
|
306
|
-
follow_location: true
|
307
|
-
)
|
308
|
-
|
309
|
-
bool = nil
|
310
|
-
http.get( Options.session.check_url, http_options ) do |response|
|
311
|
-
bool = !!response.body.match( Options.session.check_pattern )
|
312
|
-
block.call( bool ) if block
|
313
|
-
end
|
314
|
-
bool
|
315
|
-
end
|
316
|
-
|
317
|
-
# @return [Bool]
|
318
|
-
# `true` if a login check exists, `false` otherwise.
|
319
|
-
def has_login_check?
|
320
|
-
!!(Options.session.check_url && Options.session.check_pattern)
|
321
|
-
end
|
322
|
-
|
323
|
-
# @return [HTTP::Client]
|
324
|
-
def http
|
325
|
-
HTTP::Client
|
326
|
-
end
|
327
|
-
|
328
|
-
def has_browser?
|
329
|
-
Browser.has_executable? && Options.scope.dom_depth_limit > 0
|
330
|
-
end
|
331
|
-
|
332
|
-
private
|
333
|
-
|
334
364
|
def shutdown_browser
|
335
365
|
return if !@browser
|
336
366
|
|
@@ -44,6 +44,9 @@ class Framework
|
|
44
44
|
# @return [Support::LookUp::HashSet]
|
45
45
|
attr_reader :url_queue_filter
|
46
46
|
|
47
|
+
# @return [Support::LookUp::HashSet]
|
48
|
+
attr_reader :element_pre_check_filter
|
49
|
+
|
47
50
|
# @return [Set]
|
48
51
|
attr_reader :browser_skip_states
|
49
52
|
|
@@ -71,6 +74,8 @@ class Framework
|
|
71
74
|
@page_queue_filter = Support::LookUp::HashSet.new( hasher: :persistent_hash )
|
72
75
|
@url_queue_filter = Support::LookUp::HashSet.new( hasher: :persistent_hash )
|
73
76
|
|
77
|
+
@element_pre_check_filter = Support::LookUp::HashSet.new( hasher: :coverage_hash )
|
78
|
+
|
74
79
|
@running = false
|
75
80
|
@pre_pause_status = nil
|
76
81
|
|
@@ -185,6 +190,25 @@ class Framework
|
|
185
190
|
@browser_skip_states.merge states
|
186
191
|
end
|
187
192
|
|
193
|
+
# @param [#coverage_hash] e
|
194
|
+
#
|
195
|
+
# @return [Bool]
|
196
|
+
# `true` if the element has already been seen (based on the
|
197
|
+
# {#element_pre_check_filter}), `false` otherwise.
|
198
|
+
#
|
199
|
+
# @see #element_checked
|
200
|
+
def element_checked?( e )
|
201
|
+
@element_pre_check_filter.include? e
|
202
|
+
end
|
203
|
+
|
204
|
+
# @param [Page] e
|
205
|
+
# Element to mark as seen.
|
206
|
+
#
|
207
|
+
# @see #element_checked?
|
208
|
+
def element_checked( e )
|
209
|
+
@element_pre_check_filter << e
|
210
|
+
end
|
211
|
+
|
188
212
|
def running?
|
189
213
|
!!@running
|
190
214
|
end
|
@@ -377,9 +401,8 @@ class Framework
|
|
377
401
|
|
378
402
|
rpc.dump( "#{directory}/rpc/" )
|
379
403
|
|
380
|
-
%w(page_queue_filter url_queue_filter
|
381
|
-
audited_page_count).each do |attribute|
|
382
|
-
|
404
|
+
%w(element_pre_check_filter page_queue_filter url_queue_filter
|
405
|
+
browser_skip_states audited_page_count).each do |attribute|
|
383
406
|
IO.binwrite( "#{directory}/#{attribute}", Marshal.dump( send(attribute) ) )
|
384
407
|
end
|
385
408
|
end
|
@@ -389,8 +412,12 @@ class Framework
|
|
389
412
|
|
390
413
|
framework.rpc = RPC.load( "#{directory}/rpc/" )
|
391
414
|
|
392
|
-
%w(page_queue_filter url_queue_filter
|
393
|
-
|
415
|
+
%w(element_pre_check_filter page_queue_filter url_queue_filter
|
416
|
+
browser_skip_states).each do |attribute|
|
417
|
+
path = "#{directory}/#{attribute}"
|
418
|
+
next if !File.exist?( path )
|
419
|
+
|
420
|
+
framework.send(attribute).merge Marshal.load( IO.binread( path ) )
|
394
421
|
end
|
395
422
|
|
396
423
|
framework.audited_page_count = Marshal.load( IO.binread( "#{directory}/audited_page_count" ) )
|
@@ -400,6 +427,8 @@ class Framework
|
|
400
427
|
def clear
|
401
428
|
rpc.clear
|
402
429
|
|
430
|
+
@element_pre_check_filter.clear
|
431
|
+
|
403
432
|
@page_queue_filter.clear
|
404
433
|
@url_queue_filter.clear
|
405
434
|
|