jira_scan 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +15 -0
  2. data/bin/jira-scan +164 -0
  3. data/lib/jira_scan.rb +326 -0
  4. metadata +47 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YmVmMjdhMTg1NThlZmMzNDBjNzE3Y2Q0MjY3NzQ1MTY5OTRlMjc5Yg==
5
+ data.tar.gz: !binary |-
6
+ OTk3Mjc4M2FiNDU0MzdlYjU0ZjhiYTA2MzdlNWRjZjk2YWQ5NzQxOA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZDk2MTdhZjM5ZWI3Y2NjYWIwNzI0MzhiZjU1NzNiNDg3NDU2NjViODJlZjQ2
10
+ ZDg4YTg3NGQ1YTg4NGMwZTMyMDNlNjVkNzhhOGMxZTEzMGY5ODdiNDgzMzkw
11
+ ZGYwM2E1MDJkZjkxZDY2NzdjNGU3NTMzZDEzNmY0Zjk2MzQ1ZDI=
12
+ data.tar.gz: !binary |-
13
+ NzA2ODczZTI3YzAxYWFhYmEyMWMyMjJjYjJhYzIyN2I0ZDkzYjllZThiMTZi
14
+ MTNhMWNjYmZkZThhZTNkNThmY2UzOWJjMTQ5YTk2M2NjZWY2ZTRmNDQzZjAy
15
+ Y2RiMzZhMmQ1YzUzNDg0ZDIxNDA3YjEwOWNkZmQzMjBmMzgzYmE=
data/bin/jira-scan ADDED
@@ -0,0 +1,164 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # This file is part of JiraScan
4
+ # https://github.com/bcoles/jira_scan
5
+ #
6
+
7
+ require 'jira_scan'
8
+ require 'optparse'
9
+ require 'terminal-table'
10
+ require 'resolv'
11
+
12
+ def banner
13
+ puts "
14
+ _ _ _____
15
+ | (_) / ____|
16
+ | |_ _ __ __ _| (___ ___ __ _ _ __
17
+ _ | | | '__/ _` |\\___ \\ / __/ _` | '_ \\
18
+ | |__| | | | | (_| |____) | (_| (_| | | | |
19
+ \\____/|_|_| \\__,_|_____/ \\___\\__,_|_| |_|
20
+ version 0.0.2"
21
+ puts
22
+ puts '-' * 60
23
+ end
24
+
25
+ banner
26
+ options = {}
27
+ opts = OptionParser.new do |o|
28
+ o.banner = 'Usage: jira-scan [options]'
29
+
30
+ o.on('-u URL', '--url URL', 'Jira URL to scan') do |v|
31
+ unless v.match(%r{\Ahttps?://})
32
+ puts "- Invalid URL: #{v}"
33
+ exit(1)
34
+ end
35
+ options[:url] = v
36
+ end
37
+
38
+ o.on('-s', '--skip', 'Skip check for Jira') do
39
+ options[:skip] = true
40
+ end
41
+
42
+ o.on('-v', '--verbose', 'Enable verbose output') do
43
+ options[:verbose] = true
44
+ end
45
+
46
+ o.on('-h', '--help', 'Show this help') do
47
+ puts opts
48
+ exit
49
+ end
50
+ end
51
+
52
+ opts.parse!
53
+
54
+ $VERBOSE = true unless options[:verbose].nil?
55
+ @check = true unless options[:skip]
56
+
57
+ if options[:url].nil?
58
+ puts opts
59
+ exit(1)
60
+ end
61
+
62
+ def scan(url)
63
+ puts "Scan started at #{Time.now.getutc}"
64
+ puts "URL: #{url}"
65
+
66
+ # parse URL
67
+ target = nil
68
+ begin
69
+ target = URI::parse(url.split('?').first)
70
+ rescue
71
+ puts "- Could not parse target URL: #{url}"
72
+ end
73
+ exit(1) if target.nil?
74
+
75
+ # resolve IP address
76
+ begin
77
+ ip = Resolv.getaddress(target.host).to_s
78
+ puts "IP: #{ip}" unless ip.nil?
79
+ rescue
80
+ puts "- Could not resolve hostname #{target.host}"
81
+ end
82
+
83
+ puts "Port: #{target.port}"
84
+ puts '-' * 60
85
+
86
+ # Check if the URL is Jira
87
+ if @check
88
+ unless JiraScan::isJira(url)
89
+ puts '- Jira not found'
90
+ exit(1)
91
+ end
92
+ puts '+ Found Jira'
93
+ end
94
+
95
+ # Get Jira version
96
+ version = JiraScan::getVersion(url)
97
+ puts "+ Version: #{version}" if version
98
+
99
+ # Dev mode enabled
100
+ dev_mode = JiraScan::devMode(url)
101
+ puts '+ Dev mode is enabled' if dev_mode
102
+
103
+ # User registration enabled
104
+ register = JiraScan::userRegistration(url)
105
+ puts '+ User registration is enabled' if register
106
+
107
+ # Check if User Picker Browser is accessible
108
+ user_picker = JiraScan::userPickerBrowser(url)
109
+ if user_picker
110
+ puts '+ User Picker Browser is available'
111
+ # Retrieve list of first 1,000 users
112
+ users = JiraScan::getUsersFromUserPicker(url)
113
+ unless users.empty?
114
+ puts "+ Found users (#{users.length}):"
115
+ table = Terminal::Table.new :headings => ['Username', 'Full Name', 'Email'], :rows => users
116
+ puts table
117
+ end
118
+ end
119
+
120
+ # Check if REST User Picker is accessible
121
+ rest_user_picker = JiraScan::restUserPicker(url)
122
+ puts "+ REST UserPicker is available" if rest_user_picker
123
+
124
+ # Check if REST Group User Picker is accessible
125
+ rest_group_user_picker = JiraScan::restGroupUserPicker(url)
126
+ puts "+ REST GroupUserPicker is available" if rest_group_user_picker
127
+
128
+ # Check if ViewUserHover.jspa is accessible
129
+ view_user_hover = JiraScan::viewUserHover(url)
130
+ puts "+ ViewUserHover.jspa is available" if view_user_hover
131
+
132
+ # Check if META-INF contents are accessible
133
+ meta_inf = JiraScan::metaInf(url)
134
+ puts '+ META-INF directory contents are accessible' if meta_inf
135
+
136
+ # Retrieve list of dashboards
137
+ dashboards = JiraScan::getDashboards(url)
138
+ unless dashboards.empty?
139
+ puts "+ Found dashboards (#{dashboards.length}):"
140
+ table = Terminal::Table.new :headings => ['ID', 'Name'], :rows => dashboards
141
+ puts table
142
+ end
143
+
144
+ # Retrieve list of popular filters
145
+ filters = JiraScan::getPopularFilters(url)
146
+ unless filters.empty?
147
+ puts "+ Found popular filters (#{filters.length}):"
148
+ table = Terminal::Table.new :headings => ['Filter Name'], :rows => filters
149
+ puts table
150
+ end
151
+
152
+ # Retrieve list of field names
153
+ field_names = JiraScan::getFieldNames(url)
154
+ unless field_names.empty?
155
+ puts "+ Found field names (#{field_names.length}):"
156
+ table = Terminal::Table.new :headings => ['Name', 'ID', 'Key', 'IsShown', 'Last Viewed'], :rows => field_names
157
+ puts table
158
+ end
159
+
160
+ puts "Scan finished at #{Time.now.getutc}"
161
+ puts '-' * 60
162
+ end
163
+
164
+ scan(options[:url])
data/lib/jira_scan.rb ADDED
@@ -0,0 +1,326 @@
1
+ #
2
+ # This file is part of JiraScan
3
+ # https://github.com/bcoles/jira_scan
4
+ #
5
+
6
+ require 'uri'
7
+ require 'cgi'
8
+ require 'json'
9
+ require 'net/http'
10
+ require 'openssl'
11
+
12
+ class JiraScan
13
+ VERSION = '0.0.2'.freeze
14
+
15
+ #
16
+ # Check if Jira
17
+ #
18
+ # @param [String] URL
19
+ #
20
+ # @return [Boolean]
21
+ #
22
+ def self.isJira(url)
23
+ url += '/' unless url.to_s.end_with? '/'
24
+ res = sendHttpRequest("#{url}secure/Dashboard.jspa")
25
+
26
+ return false unless res
27
+ return false unless res.code.to_i == 200
28
+
29
+ res.body.to_s.include?('JIRA')
30
+ end
31
+
32
+ #
33
+ # Get Jira version
34
+ #
35
+ # @param [String] URL
36
+ #
37
+ # @return [String] Jira version
38
+ #
39
+ def self.getVersion(url)
40
+ url += '/' unless url.to_s.end_with? '/'
41
+ res = sendHttpRequest("#{url}secure/Dashboard.jspa")
42
+
43
+ return unless res
44
+ return unless res.code.to_i == 200
45
+
46
+ version = res.body.to_s.scan(%r{<meta name="ajs-version-number" content="([\d\.]+)">}).flatten.first
47
+ build = res.body.to_s.scan(%r{<meta name="ajs-build-number" content="(\d+)">}).flatten.first
48
+
49
+ unless version && build
50
+ if res.body.to_s =~ /Version: ([\d\.]+)-#(\d+)/
51
+ version = $1
52
+ build = $2
53
+ else
54
+ return
55
+ end
56
+ end
57
+
58
+ "#{version}-##{build}"
59
+ end
60
+
61
+ #
62
+ # Check if dev mode is enabled
63
+ #
64
+ # @param [String] URL
65
+ #
66
+ # @return [Boolean]
67
+ #
68
+ def self.devMode(url)
69
+ url += '/' unless url.to_s.end_with? '/'
70
+ res = sendHttpRequest(url)
71
+
72
+ return false unless res
73
+ return false unless res.code.to_i == 200
74
+
75
+ res.body.to_s.include?('<meta name="ajs-dev-mode" content="true">')
76
+ end
77
+
78
+ #
79
+ # Check if account registration is enabled
80
+ #
81
+ # @param [String] URL
82
+ #
83
+ # @return [Boolean]
84
+ #
85
+ def self.userRegistration(url)
86
+ url += '/' unless url.to_s.end_with? '/'
87
+ res = sendHttpRequest("#{url}secure/Signup!default.jspa")
88
+
89
+ return false unless res
90
+ return false unless res.code.to_i == 200
91
+
92
+ res.body.to_s.include?('<h1>Sign up</h1>')
93
+ end
94
+
95
+ #
96
+ # Check if unauthenticated access to UserPickerBrowser.jspa is allowed
97
+ #
98
+ # @param [String] URL
99
+ #
100
+ # @return [Boolean]
101
+ #
102
+ def self.userPickerBrowser(url)
103
+ url += '/' unless url.to_s.end_with? '/'
104
+ res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa")
105
+
106
+ return false unless res
107
+ return false unless res.code.to_i == 200
108
+
109
+ res.body.to_s.include?('<h1>User Picker</h1>')
110
+ end
111
+
112
+ #
113
+ # Retrieve list of users from UserPickerBrowser
114
+ #
115
+ # @param [String] URL
116
+ #
117
+ # @return [Array] list of first 1,000 users
118
+ #
119
+ def self.getUsersFromUserPickerBrowser(url)
120
+ url += '/' unless url.to_s.end_with? '/'
121
+ max = 1_000
122
+ res = sendHttpRequest("#{url}secure/popups/UserPickerBrowser.jspa?max=#{max}")
123
+
124
+ return [] unless res && res.code.to_i == 200 && res.body.to_s.include?('<h1>User Picker</h1>')
125
+
126
+ users = []
127
+ if res.body.to_s.include? 'cell-type-email'
128
+ res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>\s+<td data-cell-type="email" class="cell-type-email">(.*?)</td>}m).each do |u|
129
+ users << u
130
+ end
131
+ else
132
+ res.body.to_s.scan(%r{<td data-cell-type="name" class="user-name">(.*?)</td>\s+<td data-cell-type="fullname" >(.*?)</td>}m).each do |u|
133
+ users << u
134
+ end
135
+ end
136
+
137
+ users
138
+ rescue
139
+ []
140
+ end
141
+
142
+ #
143
+ # Check if unauthenticated access to REST UserPicker is allowed (CVE-2019-3403)
144
+ #
145
+ # @param [String] URL
146
+ #
147
+ # @return [Boolean]
148
+ #
149
+ def self.restUserPicker(url)
150
+ url += '/' unless url.to_s.end_with? '/'
151
+ res = sendHttpRequest("#{url}rest/api/latest/user/picker")
152
+
153
+ return false unless res
154
+ return false unless res.code.to_i == 400
155
+
156
+ res.body.to_s.include?('The username query parameter was not provided')
157
+ end
158
+
159
+ #
160
+ # Check if unauthenticated access to REST GroupUserPicker is allowed (CVE-2019-8449)
161
+ #
162
+ # @param [String] URL
163
+ #
164
+ # @return [Boolean]
165
+ #
166
+ def self.restGroupUserPicker(url)
167
+ url += '/' unless url.to_s.end_with? '/'
168
+ res = sendHttpRequest("#{url}rest/api/latest/groupuserpicker")
169
+
170
+ return false unless res
171
+ return false unless res.code.to_i == 400
172
+
173
+ res.body.to_s.include?('The username query parameter was not provided')
174
+ end
175
+
176
+ #
177
+ # Check if unauthenticated access to ViewUserHover.jspa is allowed (CVE-2020-14181)
178
+ #
179
+ # @param [String] URL
180
+ #
181
+ # @return [Boolean]
182
+ #
183
+ def self.viewUserHover(url)
184
+ url += '/' unless url.to_s.end_with? '/'
185
+ res = sendHttpRequest("#{url}secure/ViewUserHover.jspa")
186
+
187
+ return false unless res
188
+ return false unless res.code.to_i == 200
189
+
190
+ res.body.to_s.include?('User does not exist')
191
+ end
192
+
193
+ #
194
+ # Check if META-INF contents are accessible (CVE-2019-8442)
195
+ #
196
+ # @param [String] URL
197
+ #
198
+ # @return [Boolean]
199
+ #
200
+ def self.metaInf(url)
201
+ url += '/' unless url.to_s.end_with? '/'
202
+ res = sendHttpRequest("#{url}s/#{rand(36**6).to_s(36)}/_/META-INF/maven/com.atlassian.jira/atlassian-jira-webapp/pom.xml")
203
+
204
+ return false unless res
205
+ return false unless res.code.to_i == 200
206
+
207
+ res.body.to_s.start_with?('<project')
208
+ end
209
+
210
+ #
211
+ # Retrieve list of popular filters
212
+ #
213
+ # @param [String] URL
214
+ #
215
+ # @return [Array] list of popular filters
216
+ #
217
+ def self.getPopularFilters(url)
218
+ url += '/' unless url.to_s.end_with? '/'
219
+ res = sendHttpRequest("#{url}secure/ManageFilters.jspa?filter=popular&filterView=popular")
220
+
221
+ return [] unless res
222
+ return [] unless res.code.to_i == 200
223
+ return [] unless res.body.to_s.include?('<h1>Manage Filters</h1>')
224
+
225
+ return res.body.to_s.scan(%r{requestId=\d+">(.+?)</a>}) if res.body.to_s =~ /requestId=\d/
226
+ return res.body.to_s.scan(%r{filter=\d+">(.+?)</a>}) if res.body.to_s =~ /filter=\d/
227
+
228
+ []
229
+ rescue
230
+ []
231
+ end
232
+
233
+ #
234
+ # Retrieve list of dashboards
235
+ #
236
+ # @param [String] URL
237
+ #
238
+ # @return [Array] list of dashboards
239
+ #
240
+ def self.getDashboards(url)
241
+ url += '/' unless url.to_s.end_with? '/'
242
+ max = 1_000
243
+ res = sendHttpRequest("#{url}rest/api/2/dashboard?maxResults=#{max}")
244
+
245
+ return [] unless res
246
+ return [] unless res.code.to_i == 200
247
+ return [] unless res.body.to_s.start_with?('{"startAt"')
248
+
249
+ JSON.parse(res.body.to_s, symbolize_names: true)[:dashboards].map {|d| [d[:id], d[:name]] }
250
+ rescue
251
+ []
252
+ end
253
+
254
+ #
255
+ # Retrieve list of field names from QueryComponent!Default.jspa (CVE-2020-14179)
256
+ #
257
+ # @param [String] URL
258
+ #
259
+ # @return [Array] list of field names
260
+ #
261
+ def self.getFieldNames(url)
262
+ url += '/' unless url.to_s.end_with? '/'
263
+ res = sendHttpRequest("#{url}secure/QueryComponent!Default.jspa")
264
+
265
+ return [] unless res
266
+ return [] unless res.code.to_i == 200
267
+ return [] unless res.body.to_s.start_with?('{"searchers"')
268
+
269
+ searchers = JSON.parse(res.body.to_s)["searchers"]
270
+ return [] if searchers.empty?
271
+
272
+ groups = searchers['groups']
273
+ return [] if groups.empty?
274
+
275
+ field_names = []
276
+ groups.each do |g|
277
+ g['searchers'].each do |s|
278
+ field_names << s
279
+ end
280
+ end
281
+
282
+ JSON.parse(field_names.to_json, symbolize_names: true).map {|f| [f[:name], f[:id], f[:key], f[:isShown].to_s, f[:lastViewed]] }
283
+ rescue
284
+ []
285
+ end
286
+
287
+ private
288
+
289
+ #
290
+ # Fetch URL
291
+ #
292
+ # @param [String] URL
293
+ #
294
+ # @return [Net::HTTPResponse] HTTP response
295
+ #
296
+ def self.sendHttpRequest(url)
297
+ target = URI.parse(url)
298
+ puts "* Fetching #{target}" if $VERBOSE
299
+ http = Net::HTTP.new(target.host, target.port)
300
+ if target.scheme.to_s.eql?('https')
301
+ http.use_ssl = true
302
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
303
+ # http.verify_mode = OpenSSL::SSL::VERIFY_PEER
304
+ end
305
+ http.open_timeout = 20
306
+ http.read_timeout = 20
307
+ headers = {}
308
+ headers['User-Agent'] = "JiraScan/#{VERSION}"
309
+ headers['Accept-Encoding'] = 'gzip,deflate'
310
+
311
+ begin
312
+ res = http.request(Net::HTTP::Get.new(target, headers.to_hash))
313
+ if res.body && res['Content-Encoding'].eql?('gzip')
314
+ sio = StringIO.new(res.body)
315
+ gz = Zlib::GzipReader.new(sio)
316
+ res.body = gz.read
317
+ end
318
+ rescue Timeout::Error, Errno::ETIMEDOUT
319
+ puts "- Error: Timeout retrieving #{target}" if $VERBOSE
320
+ rescue => e
321
+ puts "- Error: Could not retrieve URL #{target}\n#{e}" if $VERBOSE
322
+ end
323
+ puts "+ Received reply (#{res.body.length} bytes)" if $VERBOSE
324
+ res
325
+ end
326
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jira_scan
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Brendan Coles
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-03-23 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A simple remote scanner for Atlassian Jira
14
+ email: bcoles@gmail.com
15
+ executables:
16
+ - jira-scan
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/jira-scan
21
+ - lib/jira_scan.rb
22
+ homepage: https://github.com/bcoles/jira_scan
23
+ licenses:
24
+ - MIT
25
+ metadata: {}
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ! '>='
33
+ - !ruby/object:Gem::Version
34
+ version: 2.0.0
35
+ required_rubygems_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 2.2.2
43
+ signing_key:
44
+ specification_version: 4
45
+ summary: Jira scanner
46
+ test_files: []
47
+ has_rdoc: