jira_scan 0.0.2

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 (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: