watobo 0.9.20 → 0.9.21
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +54 -2
- data/README.md +1 -1
- data/config/scanner.yml +1 -0
- data/custom-views/prettify-json.rb +19 -0
- data/lib/watobo/adapters/file/marshal_store.rb +297 -0
- data/lib/watobo/adapters.rb +2 -1
- data/lib/watobo/core/active_check.rb +4 -0
- data/lib/watobo/core/chat.rb +8 -0
- data/lib/watobo/core/chats.rb +2 -1
- data/lib/watobo/core/cookie.rb +3 -3
- data/lib/watobo/core/finding.rb +7 -0
- data/lib/watobo/core/request.rb +3 -3
- data/lib/watobo/core/session.rb +6 -2
- data/lib/watobo/framework/init_modules.rb +18 -16
- data/lib/watobo/gui/conversation_table.rb +13 -16
- data/lib/watobo/gui/conversation_table_ctrl2.rb +1 -0
- data/lib/watobo/gui/custom_viewer.rb +101 -76
- data/lib/watobo/gui/define_scope_frame.rb +44 -10
- data/lib/watobo/gui/edit_scope_dialog.rb +1 -1
- data/lib/watobo/gui/fuzzer_gui.rb +61 -23
- data/lib/watobo/gui/main_window.rb +1 -1
- data/lib/watobo/gui/scanner_settings_dialog.rb +15 -0
- data/lib/watobo/http/data/json.rb +6 -0
- data/lib/watobo/interceptor/html/favicon.ico +0 -0
- data/lib/watobo/interceptor/html/index.html +13 -0
- data/lib/watobo/interceptor/proxy.rb +70 -18
- data/lib/watobo/mixins/httpparser.rb +26 -16
- data/lib/watobo/mixins/shapers.rb +49 -5
- data/lib/watobo/mixins/transcoders.rb +8 -8
- data/lib/watobo/sockets/connection.rb +1 -1
- data/lib/watobo/utils/load_chat.rb +62 -0
- data/lib/watobo/utils/response_hash.rb +3 -3
- data/lib/watobo.rb +1 -1
- data/modules/active/cq5/cq5_default_selectors.rb +116 -0
- data/modules/active/cq5/cqp_user_enumeration.rb +134 -0
- data/modules/active/struts2/include_params_ognl.rb +1 -1
- data/modules/active/xml/xml_xxe.rb +6 -1
- data/modules/passive/disclosure_domino.rb +1 -1
- data/modules/passive/in_script_parameter.rb +9 -4
- data/plugins/aem/aem.rb +21 -0
- data/plugins/aem/gui/main.rb +128 -0
- data/plugins/aem/gui/tree_view.rb +180 -0
- data/plugins/aem/icons/aem.ico +0 -0
- data/plugins/aem/lib/agent.rb +140 -0
- data/plugins/aem/lib/dispatcher.rb +53 -0
- data/plugins/aem/lib/engine.rb +187 -0
- data/plugins/filefinder/dbs/cq5.db +23 -0
- data/plugins/filefinder/dbs/subs-big.lst +44 -44
- data/plugins/filefinder/filefinder.rb +4 -4
- data/plugins/sqlmap/lib/sqlmap_ctrl.rb +11 -10
- metadata +16 -2
@@ -43,7 +43,7 @@ module Watobo#:nodoc: all
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def replaceURL(new_url)
|
46
|
-
self.first.gsub!(/(^[^[:space:]]{1,})(.*) (HTTP.*)/i,"\\1 #{new_url} \\3")
|
46
|
+
self.first.gsub!(/(^[^[:space:]]{1,}) (.*) (HTTP.*)/i,"\\1 #{new_url} \\3")
|
47
47
|
end
|
48
48
|
|
49
49
|
def replaceQuery(new_query)
|
@@ -63,6 +63,53 @@ module Watobo#:nodoc: all
|
|
63
63
|
self.first.gsub!(/(^[^[:space:]]{1,} https?:\/\/[\-0-9a-zA-Z.]*[:0-9]{0,6}\/)(.*)( HTTP\/.*)/, "\\1#{dir}\\3")
|
64
64
|
end
|
65
65
|
|
66
|
+
#
|
67
|
+
# set a new file extension, e.g. mysite.html to mysite.php
|
68
|
+
# if no extension nor a file is given, the new extension will only be appended
|
69
|
+
# note: the first leading dot will be removed
|
70
|
+
# possible preferences are:
|
71
|
+
# :keep_query => keeps query parameters
|
72
|
+
# default-set is empty
|
73
|
+
def set_file_extension(nxt, *prefs)
|
74
|
+
return self.first if nxt.nil?
|
75
|
+
nxt.gsub!(/^\./,'')
|
76
|
+
s = "#{self.first}"
|
77
|
+
fend = nil
|
78
|
+
pend = nil
|
79
|
+
|
80
|
+
pstart = s.index('?')
|
81
|
+
pend = s.rindex(/ HTTP\//)
|
82
|
+
|
83
|
+
fend = (pstart - 1) unless pstart.nil?
|
84
|
+
fend = ( pend - 1 ) if fend.nil?
|
85
|
+
|
86
|
+
return self.first if fend.nil?
|
87
|
+
|
88
|
+
fstart = s.rindex('/', fend)
|
89
|
+
unless s[fstart-1] == '/'
|
90
|
+
fstart += 1 unless fstart.nil?
|
91
|
+
else
|
92
|
+
fstart = fend
|
93
|
+
end
|
94
|
+
|
95
|
+
fname = s[fstart..fend]
|
96
|
+
fname.gsub!(/\..*/,'')
|
97
|
+
fname << ".#{nxt}"
|
98
|
+
|
99
|
+
ns = s[0..fstart-1]
|
100
|
+
ns << fname
|
101
|
+
|
102
|
+
if prefs.include? :keep_query
|
103
|
+
unless pstart.nil?
|
104
|
+
ns << s[pstart..pend]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
ns << s[pend..-1]
|
109
|
+
|
110
|
+
self.first.replace ns
|
111
|
+
end
|
112
|
+
|
66
113
|
def appendDir(dir)
|
67
114
|
dir.strip!
|
68
115
|
dir.gsub!(/^\//,"")
|
@@ -245,10 +292,7 @@ module Watobo#:nodoc: all
|
|
245
292
|
|
246
293
|
def fix_content_length
|
247
294
|
return false if self.body.nil?
|
248
|
-
#
|
249
|
-
# Workaround: toHex and then calculate length ... very time consuming :(
|
250
|
-
#
|
251
|
-
#blen = self.body.unpack("H*")[0].length / 2
|
295
|
+
# had trouble with length calculation of binary data in multipart request
|
252
296
|
blen = self.body.force_encoding("ASCII-8BIT").length
|
253
297
|
set_header("Content-Length" , blen)
|
254
298
|
end
|
@@ -20,23 +20,23 @@ module Watobo#:nodoc: all
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def b64decode
|
23
|
-
err_count = 0
|
24
|
-
b64string = self
|
23
|
+
err_count = 0
|
25
24
|
begin
|
25
|
+
b64string = self.force_encoding('ASCII-8BIT')
|
26
26
|
rs = Base64.strict_decode64(b64string)
|
27
27
|
#rs = Base64.decode64(b64string)
|
28
28
|
return rs
|
29
29
|
rescue
|
30
|
-
b64string.gsub!(/.$/,'')
|
31
|
-
err_count += 1
|
32
|
-
retry if err_count < 4
|
33
|
-
return
|
30
|
+
#b64string.gsub!(/.$/,'')
|
31
|
+
#err_count += 1
|
32
|
+
#retry if err_count < 4
|
33
|
+
return self.to_s
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
37
|
def b64encode
|
38
38
|
begin
|
39
|
-
plain = self
|
39
|
+
plain = self.force_encoding('ASCII-8BIT')
|
40
40
|
#rs = Base64.strict_encode64(plain)
|
41
41
|
rs = Base64.strict_encode64(plain)
|
42
42
|
# we only need a simple string without linebreaks
|
@@ -44,7 +44,7 @@ module Watobo#:nodoc: all
|
|
44
44
|
#rs.strip!
|
45
45
|
return rs
|
46
46
|
rescue
|
47
|
-
return
|
47
|
+
return self.to_s
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -95,7 +95,7 @@ module Watobo#:nodoc: all
|
|
95
95
|
return nil
|
96
96
|
end
|
97
97
|
|
98
|
-
header = [ "HTTP/1.1
|
98
|
+
header = [ "HTTP/1.1 502 Bad Gateway\r\n", "Server: WATOBO\r\n", "Content-Length: #{msg.length.to_i}\r\n", "Content-Type: text/html\r\n", "\r\n", "#{msg}" ] unless msg.nil?
|
99
99
|
|
100
100
|
response = Watobo::Response.new header
|
101
101
|
# update_sids(header)
|
@@ -11,6 +11,44 @@
|
|
11
11
|
module Watobo#:nodoc: all
|
12
12
|
module Utils
|
13
13
|
|
14
|
+
def Utils.loadChatMarshal(file)
|
15
|
+
begin
|
16
|
+
request = []
|
17
|
+
response = []
|
18
|
+
if File.exists?(file) then
|
19
|
+
puts "LoadChatMarshal: #{file}" if $DEBUG
|
20
|
+
settings = {}
|
21
|
+
File.open(file,"rb") { |fh|
|
22
|
+
settings = Marshal::load(fh.read)
|
23
|
+
request = settings[:request]
|
24
|
+
response = settings[:response]
|
25
|
+
settings.delete(:response)
|
26
|
+
settings.delete(:request)
|
27
|
+
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
chat = Watobo::Chat.new(request, response, settings)
|
32
|
+
chat.file = file
|
33
|
+
|
34
|
+
return chat
|
35
|
+
|
36
|
+
else
|
37
|
+
puts "* file #{file} not found"
|
38
|
+
return nil
|
39
|
+
end
|
40
|
+
rescue Psych::SyntaxError
|
41
|
+
puts "!!! Malformed File #{file}"
|
42
|
+
rescue => bang
|
43
|
+
puts "! could not load chat from file #{file}"
|
44
|
+
puts bang
|
45
|
+
puts bang.backtrace
|
46
|
+
#puts cdata
|
47
|
+
#puts bang
|
48
|
+
#puts bang.backtrace if $DEBUG
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
14
52
|
# loadChat returns a chat object imported from a yaml file
|
15
53
|
def Utils.loadChatYAML(file)
|
16
54
|
begin
|
@@ -68,6 +106,30 @@ module Watobo#:nodoc: all
|
|
68
106
|
end
|
69
107
|
|
70
108
|
|
109
|
+
def Utils.loadFindingMarshal(file)
|
110
|
+
puts "LoadFindingMarshal: #{file}" if $DEBUG
|
111
|
+
if File.exists?(file) then
|
112
|
+
begin
|
113
|
+
fdata = nil
|
114
|
+
|
115
|
+
File.open(file,"rb") {|f|
|
116
|
+
fdata = Marshal::load(f.read)
|
117
|
+
}
|
118
|
+
|
119
|
+
finding = Watobo::Finding.new(fdata[:request], fdata[:response], fdata[:details])
|
120
|
+
|
121
|
+
return finding
|
122
|
+
rescue => bang
|
123
|
+
puts bang
|
124
|
+
puts "could not load finding #{file}"
|
125
|
+
return nil
|
126
|
+
end
|
127
|
+
else
|
128
|
+
# puts "* file #{file} not found"
|
129
|
+
return nil
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
71
133
|
def Utils.loadFindingYAML(file)
|
72
134
|
puts "LoadFindingYAML: #{file}" if $DEBUG
|
73
135
|
if File.exists?(file) then
|
@@ -138,11 +138,11 @@ module Watobo#:nodoc: all
|
|
138
138
|
end
|
139
139
|
rescue => bang
|
140
140
|
# puts "VAL_CGI_Q: #{val_cgi_q}"
|
141
|
-
|
142
|
-
|
141
|
+
# return some random hash in case of an error
|
142
|
+
puts bang if $DEBUG
|
143
143
|
puts bang.backtrace if $DEBUG
|
144
144
|
|
145
|
-
return body, Digest::MD5.hexdigest(
|
145
|
+
return body, Digest::MD5.hexdigest(Time.now.to_f.to_s + rand(10000).to_s )
|
146
146
|
end
|
147
147
|
end
|
148
148
|
end
|
data/lib/watobo.rb
CHANGED
@@ -0,0 +1,116 @@
|
|
1
|
+
#.
|
2
|
+
# cq5_default_selectors.rb
|
3
|
+
#.
|
4
|
+
# Copyright 2014 by siberas, http://www.siberas.de
|
5
|
+
# This file is part of WATOBO (Web Application Tool Box) http://watobo.sourceforge.com
|
6
|
+
# WATOBO is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 2 of the License.
|
7
|
+
# WATOBO is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
8
|
+
# You should have received a copy of the GNU General Public License along with WATOBO; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
9
|
+
|
10
|
+
# @private
|
11
|
+
module Watobo#:nodoc: all
|
12
|
+
module Modules
|
13
|
+
module Active
|
14
|
+
module Cq5
|
15
|
+
#class Dir_indexing < Watobo::Mixin::Session
|
16
|
+
class Cq5_default_selectors < Watobo::ActiveCheck
|
17
|
+
|
18
|
+
@info.update(
|
19
|
+
:check_name => 'CQ5 Selectors', # name of check which briefly describes functionality, will be used for tree and progress views
|
20
|
+
:description => "This module checks for default selectors.", # description of checkfunction
|
21
|
+
:author => "Andreas Schmidt", # author of check
|
22
|
+
:version => "1.0", # check version
|
23
|
+
:check_group => "CQ5"
|
24
|
+
)
|
25
|
+
|
26
|
+
@finding.update(
|
27
|
+
:threat => 'Selectors can reveal sensitive information about the application, e.g. password hashes (jackrabbit). Also, the query selector enables you to perform XPATH queries on the entire repository, which could slow your system down considerably, or even cause a denial of service if run multiple times', # thread of vulnerability, e.g. loss of information
|
28
|
+
:class => "CQ5: Selectors", # vulnerability class, e.g. Stored XSS, SQL-Injection, ...
|
29
|
+
:type => FINDING_TYPE_VULN, # FINDING_TYPE_HINT, FINDING_TYPE_INFO, FINDING_TYPE_VULN
|
30
|
+
:rating => VULN_RATING_INFO
|
31
|
+
)
|
32
|
+
|
33
|
+
def initialize(project, prefs={})
|
34
|
+
super(project, prefs)
|
35
|
+
|
36
|
+
@checked_locations = []
|
37
|
+
@selectors = %w( query assets infinity children s7catalog pages feed feedentry tidy sysview docview permissions overlay 1 2 3 4 5 6 7 )
|
38
|
+
@extensions = %w( json html csv zip xml )
|
39
|
+
# specials are combinations which need one or more parameters to produce a valid result
|
40
|
+
@specials = %w( query.json?statement=%2F%2F%2A cqactions.json?path=/&depth=1&authorizableId=* permissions.overlay.json?path=/ )
|
41
|
+
|
42
|
+
@mixed = @selectors.map{|s| @extensions.map{|e| s + '.' + e } }.flatten
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
def reset()
|
47
|
+
@checked_locations = []
|
48
|
+
end
|
49
|
+
|
50
|
+
def generateChecks(chat)
|
51
|
+
path = chat.request.path
|
52
|
+
return false if @checked_locations.include? path
|
53
|
+
@checked_locations << path
|
54
|
+
|
55
|
+
test_extensions = @extensions
|
56
|
+
test_extensions.concat @specials
|
57
|
+
test_extensions.concat @mixed
|
58
|
+
|
59
|
+
test_extensions.each do |ext|
|
60
|
+
checker = proc {
|
61
|
+
begin
|
62
|
+
test_request = nil
|
63
|
+
test_response = nil
|
64
|
+
|
65
|
+
test = chat.copyRequest
|
66
|
+
|
67
|
+
# replace file extension only
|
68
|
+
|
69
|
+
test.set_file_extension(ext)
|
70
|
+
|
71
|
+
status, test_request, test_response = fileExists?(test)
|
72
|
+
|
73
|
+
if status == true and test_response.content_type != chat.response.content_type and test_response.status_code < 300
|
74
|
+
|
75
|
+
addFinding( test_request, test_response,
|
76
|
+
:test_item => "#{test_request.url}",
|
77
|
+
:proof_pattern => "#{test_response.status}",
|
78
|
+
:chat => chat,
|
79
|
+
:title => "[#{ext}]"
|
80
|
+
)
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# also test extensions on the path
|
85
|
+
test = chat.copyRequest
|
86
|
+
|
87
|
+
test.replaceFileExt(".#{ext}")
|
88
|
+
|
89
|
+
status, test_request, test_response = fileExists?(test)
|
90
|
+
|
91
|
+
if status == true and test_response.content_type != chat.response.content_type and test_response.status_code < 300
|
92
|
+
|
93
|
+
addFinding( test_request, test_response,
|
94
|
+
:test_item => "#{test_request.url}",
|
95
|
+
:proof_pattern => "#{test_response.status}",
|
96
|
+
:chat => chat,
|
97
|
+
:title => "[#{ext}]"
|
98
|
+
)
|
99
|
+
|
100
|
+
end
|
101
|
+
rescue => bang
|
102
|
+
puts bang
|
103
|
+
puts bang.backtrace if $DEBUG
|
104
|
+
end
|
105
|
+
[ test_request, test_response ]
|
106
|
+
|
107
|
+
}
|
108
|
+
yield checker
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
#.
|
2
|
+
# cqp_user_enumeration.rb
|
3
|
+
#.
|
4
|
+
# Copyright 2014 by siberas, http://www.siberas.de
|
5
|
+
# This file is part of WATOBO (Web Application Tool Box) http://watobo.sourceforge.com
|
6
|
+
# WATOBO is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 2 of the License.
|
7
|
+
# WATOBO is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
8
|
+
# You should have received a copy of the GNU General Public License along with WATOBO; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
9
|
+
|
10
|
+
# @private
|
11
|
+
module Watobo#:nodoc: all
|
12
|
+
module Modules
|
13
|
+
module Active
|
14
|
+
module Cq5
|
15
|
+
#class Dir_indexing < Watobo::Mixin::Session
|
16
|
+
class Cqp_user_enumeration < Watobo::ActiveCheck
|
17
|
+
|
18
|
+
@info.update(
|
19
|
+
:check_name => 'CQ5 CQP User Enumeration', # name of check which briefly describes functionality, will be used for tree and progress views
|
20
|
+
:description => "This module checks if CQ JSON extension is aktive and enumerates all usernames.", # description of checkfunction
|
21
|
+
:author => "Andreas Schmidt", # author of check
|
22
|
+
:version => "1.0", # check version
|
23
|
+
:check_group => "CQ5"
|
24
|
+
)
|
25
|
+
|
26
|
+
@finding.update(
|
27
|
+
:threat => 'Information Disclosure.', # thread of vulnerability, e.g. loss of information
|
28
|
+
:class => "CQ5: Users", # vulnerability class, e.g. Stored XSS, SQL-Injection, ...
|
29
|
+
:type => FINDING_TYPE_VULN, # FINDING_TYPE_HINT, FINDING_TYPE_INFO, FINDING_TYPE_VULN
|
30
|
+
:rating => VULN_RATING_INFO
|
31
|
+
)
|
32
|
+
def initialize(project, prefs={})
|
33
|
+
super(project, prefs)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset()
|
38
|
+
@checked_locations = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def generateChecks(chat)
|
42
|
+
|
43
|
+
path = chat.request.path
|
44
|
+
return false if @checked_locations.include? path
|
45
|
+
@checked_locations << path
|
46
|
+
#
|
47
|
+
# via JSON Extension
|
48
|
+
|
49
|
+
checker = proc {
|
50
|
+
begin
|
51
|
+
test_request = nil
|
52
|
+
test_response = nil
|
53
|
+
|
54
|
+
test = chat.copyRequest
|
55
|
+
|
56
|
+
test.set_file_extension('.json')
|
57
|
+
|
58
|
+
status, test_request, test_response = fileExists?(test)
|
59
|
+
|
60
|
+
if status == true and test_response.has_body?
|
61
|
+
if test_response.content_type =~ /json/
|
62
|
+
j = JSON.parse test_response.body.to_s
|
63
|
+
username = j['jcr:createdBy']
|
64
|
+
puts "\nCQ5 User: #{username}"
|
65
|
+
addFinding( test_request, test_response,
|
66
|
+
:test_item => "#{test_request.url}",
|
67
|
+
:proof_pattern => "jcr:createdBy.*#{username}",
|
68
|
+
:chat => chat,
|
69
|
+
:threat => "Usernames may help an attacker to perform authorization attacks, e.g. brute-force attacks.",
|
70
|
+
:title => "[#{username}]"
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
rescue => bang
|
76
|
+
puts bang
|
77
|
+
puts bang.backtrace if $DEBUG
|
78
|
+
end
|
79
|
+
[ test_request, test_response ]
|
80
|
+
|
81
|
+
}
|
82
|
+
yield checker
|
83
|
+
|
84
|
+
#
|
85
|
+
# via XML Extension
|
86
|
+
|
87
|
+
checker = proc {
|
88
|
+
begin
|
89
|
+
test_request = nil
|
90
|
+
test_response = nil
|
91
|
+
|
92
|
+
test = chat.copyRequest
|
93
|
+
|
94
|
+
test.set_file_extension('.xml')
|
95
|
+
|
96
|
+
status, test_request, test_response = fileExists?(test)
|
97
|
+
|
98
|
+
if status == true and test_response.has_body?
|
99
|
+
if test_response.content_type =~ /xml/
|
100
|
+
xml = Nokogiri::XML(test_response.body.to_s)
|
101
|
+
xml.traverse do |node|
|
102
|
+
next unless node.respond_to? :attributes
|
103
|
+
node.attributes.each do |attr|
|
104
|
+
if attr[0] =~ /By$/i
|
105
|
+
username = attr[1]
|
106
|
+
addFinding( test_request, test_response,
|
107
|
+
:test_item => "#{test_request.url}",
|
108
|
+
:proof_pattern => "#{attr[0]}.*#{username}",
|
109
|
+
:chat => chat,
|
110
|
+
:threat => "Usernames may help an attacker to perform authorization attacks, e.g. brute-force attacks.",
|
111
|
+
:title => "[#{username}]"
|
112
|
+
)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
rescue => bang
|
121
|
+
puts bang
|
122
|
+
puts bang.backtrace if $DEBUG
|
123
|
+
end
|
124
|
+
[ test_request, test_response ]
|
125
|
+
|
126
|
+
}
|
127
|
+
yield checker
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|